1
0
Эх сурвалжийг харах

ACR autologin. Only warns for autologin errors, as ACR registries might not be related to the user azure login, but they might have external credentials to use ACR images.

Guillaume Tardif 5 жил өмнө
parent
commit
63fd8f2fad

+ 91 - 21
aci/convert/registry_credentials.go

@@ -17,8 +17,13 @@
 package convert
 
 import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
 	"net/url"
 	"os"
+	"os/exec"
 	"strings"
 
 	"github.com/Azure/azure-sdk-for-go/profiles/latest/containerinstance/mgmt/containerinstance"
@@ -27,49 +32,50 @@ import (
 	"github.com/docker/cli/cli/config"
 	"github.com/docker/cli/cli/config/configfile"
 	"github.com/docker/cli/cli/config/types"
+	"github.com/pkg/errors"
+
+	"github.com/docker/api/aci/login"
 )
 
 // Specific username from ACR docs : https://github.com/Azure/acr/blob/master/docs/AAD-OAuth.md#getting-credentials-programatically
 const (
-	tokenUsername = "00000000-0000-0000-0000-000000000000"
-	dockerHub     = "index.docker.io"
+	tokenUsername     = "00000000-0000-0000-0000-000000000000"
+	dockerHub         = "index.docker.io"
+	acrRegistrySuffix = ".azurecr.io"
 )
 
-type registryConfLoader interface {
+type registryHelper interface {
 	getAllRegistryCredentials() (map[string]types.AuthConfig, error)
+	autoLoginAcr(registry string) error
 }
 
-type cliRegistryConfLoader struct {
+type cliRegistryHelper struct {
 	cfg *configfile.ConfigFile
 }
 
-func (c cliRegistryConfLoader) getAllRegistryCredentials() (map[string]types.AuthConfig, error) {
+func (c cliRegistryHelper) getAllRegistryCredentials() (map[string]types.AuthConfig, error) {
 	return c.cfg.GetAllCredentials()
 }
 
-func newCliRegistryConfLoader() cliRegistryConfLoader {
-	return cliRegistryConfLoader{
+func newCliRegistryConfLoader() cliRegistryHelper {
+	return cliRegistryHelper{
 		cfg: config.LoadDefaultConfigFile(os.Stderr),
 	}
 }
 
-func getRegistryCredentials(project compose.Project, registryLoader registryConfLoader) ([]containerinstance.ImageRegistryCredential, error) {
-	allCreds, err := registryLoader.getAllRegistryCredentials()
+func getRegistryCredentials(project compose.Project, helper registryHelper) ([]containerinstance.ImageRegistryCredential, error) {
+	usedRegistries, acrRegistries := getUsedRegistries(project)
+	for _, registry := range acrRegistries {
+		err := helper.autoLoginAcr(registry)
+		if err != nil {
+			fmt.Printf("Could not automatically login to %s from your Azure login. Assuming you already logged in to the ACR registry\n", registry)
+		}
+	}
+
+	allCreds, err := helper.getAllRegistryCredentials()
 	if err != nil {
 		return nil, err
 	}
-	usedRegistries := map[string]bool{}
-	for _, service := range project.Services {
-		imageName := service.Image
-		tokens := strings.Split(imageName, "/")
-		registry := tokens[0]
-		if len(tokens) == 1 { // ! image names can include "." ...
-			registry = dockerHub
-		} else if !strings.Contains(registry, ".") {
-			registry = dockerHub
-		}
-		usedRegistries[registry] = true
-	}
 	var registryCreds []containerinstance.ImageRegistryCredential
 	for name, oneCred := range allCreds {
 		parsedURL, err := url.Parse(name)
@@ -107,3 +113,67 @@ func getRegistryCredentials(project compose.Project, registryLoader registryConf
 	}
 	return registryCreds, nil
 }
+
+func getUsedRegistries(project compose.Project) (map[string]bool, []string) {
+	usedRegistries := map[string]bool{}
+	acrRegistries := []string{}
+	for _, service := range project.Services {
+		imageName := service.Image
+		tokens := strings.Split(imageName, "/")
+		registry := tokens[0]
+		if len(tokens) == 1 { // ! image names can include "." ...
+			registry = dockerHub
+		} else if !strings.Contains(registry, ".") {
+			registry = dockerHub
+		} else if strings.HasSuffix(registry, acrRegistrySuffix) {
+			acrRegistries = append(acrRegistries, registry)
+		}
+		usedRegistries[registry] = true
+	}
+	return usedRegistries, acrRegistries
+}
+
+func (c cliRegistryHelper) autoLoginAcr(registry string) error {
+	loginService, err := login.NewAzureLoginService()
+	if err != nil {
+		return err
+	}
+	token, err := loginService.GetValidToken()
+	if err != nil {
+		return err
+	}
+	tenantID, err := loginService.GetTenantID()
+	if err != nil {
+		return err
+	}
+
+	data := url.Values{
+		"grant_type":    {"access_token_refresh_token"},
+		"service":       {registry},
+		"tenant":        {tenantID},
+		"refresh_token": {token.RefreshToken},
+		"access_token":  {token.AccessToken},
+	}
+	repoAuthURL := fmt.Sprintf("https://%s/oauth2/exchange", registry)
+	res, err := http.Post(repoAuthURL, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
+	if err != nil {
+		return err
+	}
+	if res.StatusCode != 200 {
+		return errors.Errorf("error while renewing access token, status : %s", res.Status)
+	}
+	bits, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return err
+	}
+
+	type acrToken struct {
+		RefreshToken string `json:"refresh_token"`
+	}
+	newToken := acrToken{}
+	if err := json.Unmarshal(bits, &newToken); err != nil {
+		return err
+	}
+	cmd := exec.Command("docker", "login", "-p", newToken.RefreshToken, "-u", tokenUsername, registry)
+	return cmd.Run()
+}

+ 68 - 27
aci/convert/registry_credentials_test.go

@@ -17,6 +17,7 @@
 package convert
 
 import (
+	"errors"
 	"strconv"
 	"testing"
 
@@ -30,12 +31,13 @@ import (
 )
 
 const getAllCredentials = "getAllRegistryCredentials"
+const autoLoginAcr = "autoLoginAcr"
 
 func TestHubPrivateImage(t *testing.T) {
-	loader := &MockRegistryLoader{}
-	loader.On(getAllCredentials).Return(registry("https://index.docker.io", userPwdCreds("toto", "pwd")), nil)
+	registryHelper := &MockRegistryHelper{}
+	registryHelper.On(getAllCredentials).Return(registry("https://index.docker.io", userPwdCreds("toto", "pwd")), nil)
 
-	creds, err := getRegistryCredentials(composeServices("gtardif/privateimg"), loader)
+	creds, err := getRegistryCredentials(composeServices("gtardif/privateimg"), registryHelper)
 	assert.NilError(t, err)
 	assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
 		{
@@ -47,10 +49,10 @@ func TestHubPrivateImage(t *testing.T) {
 }
 
 func TestRegistryNameWithoutProtocol(t *testing.T) {
-	loader := &MockRegistryLoader{}
-	loader.On(getAllCredentials).Return(registry("index.docker.io", userPwdCreds("toto", "pwd")), nil)
+	registryHelper := &MockRegistryHelper{}
+	registryHelper.On(getAllCredentials).Return(registry("index.docker.io", userPwdCreds("toto", "pwd")), nil)
 
-	creds, err := getRegistryCredentials(composeServices("gtardif/privateimg"), loader)
+	creds, err := getRegistryCredentials(composeServices("gtardif/privateimg"), registryHelper)
 	assert.NilError(t, err)
 	assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
 		{
@@ -62,19 +64,19 @@ func TestRegistryNameWithoutProtocol(t *testing.T) {
 }
 
 func TestInvalidCredentials(t *testing.T) {
-	loader := &MockRegistryLoader{}
-	loader.On(getAllCredentials).Return(registry("18.195.159.6:444", userPwdCreds("toto", "pwd")), nil)
+	registryHelper := &MockRegistryHelper{}
+	registryHelper.On(getAllCredentials).Return(registry("18.195.159.6:444", userPwdCreds("toto", "pwd")), nil)
 
-	creds, err := getRegistryCredentials(composeServices("gtardif/privateimg"), loader)
+	creds, err := getRegistryCredentials(composeServices("gtardif/privateimg"), registryHelper)
 	assert.NilError(t, err)
 	assert.Equal(t, len(creds), 0)
 }
 
 func TestImageWithDotInName(t *testing.T) {
-	loader := &MockRegistryLoader{}
-	loader.On(getAllCredentials).Return(registry("index.docker.io", userPwdCreds("toto", "pwd")), nil)
+	registryHelper := &MockRegistryHelper{}
+	registryHelper.On(getAllCredentials).Return(registry("index.docker.io", userPwdCreds("toto", "pwd")), nil)
 
-	creds, err := getRegistryCredentials(composeServices("my.image"), loader)
+	creds, err := getRegistryCredentials(composeServices("my.image"), registryHelper)
 	assert.NilError(t, err)
 	assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
 		{
@@ -86,10 +88,11 @@ func TestImageWithDotInName(t *testing.T) {
 }
 
 func TestAcrPrivateImage(t *testing.T) {
-	loader := &MockRegistryLoader{}
-	loader.On(getAllCredentials).Return(registry("https://mycontainerregistrygta.azurecr.io", tokenCreds("123456")), nil)
+	registryHelper := &MockRegistryHelper{}
+	registryHelper.On(getAllCredentials).Return(registry("https://mycontainerregistrygta.azurecr.io", tokenCreds("123456")), nil)
+	registryHelper.On(autoLoginAcr, "mycontainerregistrygta.azurecr.io").Return(nil)
 
-	creds, err := getRegistryCredentials(composeServices("mycontainerregistrygta.azurecr.io/privateimg"), loader)
+	creds, err := getRegistryCredentials(composeServices("mycontainerregistrygta.azurecr.io/privateimg"), registryHelper)
 	assert.NilError(t, err)
 	assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
 		{
@@ -101,12 +104,13 @@ func TestAcrPrivateImage(t *testing.T) {
 }
 
 func TestAcrPrivateImageLinux(t *testing.T) {
-	loader := &MockRegistryLoader{}
+	registryHelper := &MockRegistryHelper{}
 	token := tokenCreds("123456")
 	token.Username = tokenUsername
-	loader.On(getAllCredentials).Return(registry("https://mycontainerregistrygta.azurecr.io", token), nil)
+	registryHelper.On(getAllCredentials).Return(registry("https://mycontainerregistrygta.azurecr.io", token), nil)
+	registryHelper.On(autoLoginAcr, "mycontainerregistrygta.azurecr.io").Return(nil)
 
-	creds, err := getRegistryCredentials(composeServices("mycontainerregistrygta.azurecr.io/privateimg"), loader)
+	creds, err := getRegistryCredentials(composeServices("mycontainerregistrygta.azurecr.io/privateimg"), registryHelper)
 	assert.NilError(t, err)
 	assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
 		{
@@ -118,14 +122,15 @@ func TestAcrPrivateImageLinux(t *testing.T) {
 }
 
 func TestNoMoreRegistriesThanImages(t *testing.T) {
-	loader := &MockRegistryLoader{}
+	registryHelper := &MockRegistryHelper{}
 	configs := map[string]cliconfigtypes.AuthConfig{
 		"https://mycontainerregistrygta.azurecr.io": tokenCreds("123456"),
 		"https://index.docker.io":                   userPwdCreds("toto", "pwd"),
 	}
-	loader.On(getAllCredentials).Return(configs, nil)
+	registryHelper.On(getAllCredentials).Return(configs, nil)
+	registryHelper.On(autoLoginAcr, "mycontainerregistrygta.azurecr.io").Return(nil)
 
-	creds, err := getRegistryCredentials(composeServices("mycontainerregistrygta.azurecr.io/privateimg"), loader)
+	creds, err := getRegistryCredentials(composeServices("mycontainerregistrygta.azurecr.io/privateimg"), registryHelper)
 	assert.NilError(t, err)
 	assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
 		{
@@ -135,7 +140,7 @@ func TestNoMoreRegistriesThanImages(t *testing.T) {
 		},
 	})
 
-	creds, err = getRegistryCredentials(composeServices("someuser/privateimg"), loader)
+	creds, err = getRegistryCredentials(composeServices("someuser/privateimg"), registryHelper)
 	assert.NilError(t, err)
 	assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
 		{
@@ -147,7 +152,7 @@ func TestNoMoreRegistriesThanImages(t *testing.T) {
 }
 
 func TestHubAndSeveralACRRegistries(t *testing.T) {
-	loader := &MockRegistryLoader{}
+	registryHelper := &MockRegistryHelper{}
 	configs := map[string]cliconfigtypes.AuthConfig{
 		"https://mycontainerregistry1.azurecr.io": tokenCreds("123456"),
 		"https://mycontainerregistry2.azurecr.io": tokenCreds("456789"),
@@ -155,9 +160,11 @@ func TestHubAndSeveralACRRegistries(t *testing.T) {
 		"https://index.docker.io":                 userPwdCreds("toto", "pwd"),
 		"https://other.registry.io":               userPwdCreds("user", "password"),
 	}
-	loader.On(getAllCredentials).Return(configs, nil)
+	registryHelper.On(getAllCredentials).Return(configs, nil)
+	registryHelper.On(autoLoginAcr, "mycontainerregistry1.azurecr.io").Return(nil)
+	registryHelper.On(autoLoginAcr, "mycontainerregistry2.azurecr.io").Return(nil)
 
-	creds, err := getRegistryCredentials(composeServices("mycontainerregistry1.azurecr.io/privateimg", "someuser/privateImg2", "mycontainerregistry2.azurecr.io/privateimg"), loader)
+	creds, err := getRegistryCredentials(composeServices("mycontainerregistry1.azurecr.io/privateimg", "someuser/privateImg2", "mycontainerregistry2.azurecr.io/privateimg"), registryHelper)
 	assert.NilError(t, err)
 
 	assert.Assert(t, is.Contains(creds, containerinstance.ImageRegistryCredential{
@@ -177,6 +184,35 @@ func TestHubAndSeveralACRRegistries(t *testing.T) {
 	}))
 }
 
+func TestIgnoreACRRegistryFailedAutoLogin(t *testing.T) {
+	registryHelper := &MockRegistryHelper{}
+	configs := map[string]cliconfigtypes.AuthConfig{
+		"https://mycontainerregistry1.azurecr.io": tokenCreds("123456"),
+		"https://mycontainerregistry3.azurecr.io": tokenCreds("123456789"),
+		"https://index.docker.io":                 userPwdCreds("toto", "pwd"),
+		"https://other.registry.io":               userPwdCreds("user", "password"),
+	}
+	registryHelper.On(getAllCredentials).Return(configs, nil)
+	registryHelper.On(autoLoginAcr, "mycontainerregistry1.azurecr.io").Return(nil)
+	registryHelper.On(autoLoginAcr, "mycontainerregistry2.azurecr.io").Return(errors.New("could not login"))
+
+	creds, err := getRegistryCredentials(composeServices("mycontainerregistry1.azurecr.io/privateimg", "someuser/privateImg2", "mycontainerregistry2.azurecr.io/privateimg"), registryHelper)
+	assert.NilError(t, err)
+	assert.Equal(t, len(creds), 2)
+
+	assert.Assert(t, is.Contains(creds, containerinstance.ImageRegistryCredential{
+		Server:   to.StringPtr("mycontainerregistry1.azurecr.io"),
+		Username: to.StringPtr(tokenUsername),
+		Password: to.StringPtr("123456"),
+	}))
+
+	assert.Assert(t, is.Contains(creds, containerinstance.ImageRegistryCredential{
+		Server:   to.StringPtr(dockerHub),
+		Username: to.StringPtr("toto"),
+		Password: to.StringPtr("pwd"),
+	}))
+}
+
 func composeServices(images ...string) types.Project {
 	var services []types.ServiceConfig
 	for index, name := range images {
@@ -210,11 +246,16 @@ func tokenCreds(token string) cliconfigtypes.AuthConfig {
 	}
 }
 
-type MockRegistryLoader struct {
+type MockRegistryHelper struct {
 	mock.Mock
 }
 
-func (s *MockRegistryLoader) getAllRegistryCredentials() (map[string]cliconfigtypes.AuthConfig, error) {
+func (s *MockRegistryHelper) getAllRegistryCredentials() (map[string]cliconfigtypes.AuthConfig, error) {
 	args := s.Called()
 	return args.Get(0).(map[string]cliconfigtypes.AuthConfig), args.Error(1)
 }
+
+func (s *MockRegistryHelper) autoLoginAcr(registry string) error {
+	args := s.Called(registry)
+	return args.Error(0)
+}

+ 9 - 0
aci/login/login.go

@@ -266,6 +266,15 @@ func newAuthorizerFromLoginStorePath(storeTokenPath string) (autorest.Authorizer
 	return autorest.NewBearerAuthorizer(&token), nil
 }
 
+// GetTenantID returns tenantID for current login
+func (login AzureLoginService) GetTenantID() (string, error) {
+	loginInfo, err := login.tokenStore.readToken()
+	if err != nil {
+		return "", err
+	}
+	return loginInfo.TenantID, err
+}
+
 // GetValidToken returns an access token. Refresh token if needed
 func (login *AzureLoginService) GetValidToken() (oauth2.Token, error) {
 	loginInfo, err := login.tokenStore.readToken()