From 63fd8f2fad2b45a23d6c4463ee49c4c618911af9 Mon Sep 17 00:00:00 2001 From: Guillaume Tardif Date: Fri, 7 Aug 2020 16:33:20 +0200 Subject: [PATCH] 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. --- aci/convert/registry_credentials.go | 112 ++++++++++++++++++----- aci/convert/registry_credentials_test.go | 95 +++++++++++++------ aci/login/login.go | 9 ++ 3 files changed, 168 insertions(+), 48 deletions(-) diff --git a/aci/convert/registry_credentials.go b/aci/convert/registry_credentials.go index 0e12147a9..bdb58e3b4 100644 --- a/aci/convert/registry_credentials.go +++ b/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() +} diff --git a/aci/convert/registry_credentials_test.go b/aci/convert/registry_credentials_test.go index d8a112d6b..be482aeb7 100644 --- a/aci/convert/registry_credentials_test.go +++ b/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) +} diff --git a/aci/login/login.go b/aci/login/login.go index 5d9ef68a8..d3533a4f2 100644 --- a/aci/login/login.go +++ b/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()