Browse Source

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 years ago
parent
commit
63fd8f2fad
3 changed files with 168 additions and 48 deletions
  1. 91 21
      aci/convert/registry_credentials.go
  2. 68 27
      aci/convert/registry_credentials_test.go
  3. 9 0
      aci/login/login.go

+ 91 - 21
aci/convert/registry_credentials.go

@@ -17,8 +17,13 @@
 package convert
 package convert
 
 
 import (
 import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
 	"net/url"
 	"net/url"
 	"os"
 	"os"
+	"os/exec"
 	"strings"
 	"strings"
 
 
 	"github.com/Azure/azure-sdk-for-go/profiles/latest/containerinstance/mgmt/containerinstance"
 	"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"
 	"github.com/docker/cli/cli/config/configfile"
 	"github.com/docker/cli/cli/config/configfile"
 	"github.com/docker/cli/cli/config/types"
 	"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
 // Specific username from ACR docs : https://github.com/Azure/acr/blob/master/docs/AAD-OAuth.md#getting-credentials-programatically
 const (
 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)
 	getAllRegistryCredentials() (map[string]types.AuthConfig, error)
+	autoLoginAcr(registry string) error
 }
 }
 
 
-type cliRegistryConfLoader struct {
+type cliRegistryHelper struct {
 	cfg *configfile.ConfigFile
 	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()
 	return c.cfg.GetAllCredentials()
 }
 }
 
 
-func newCliRegistryConfLoader() cliRegistryConfLoader {
-	return cliRegistryConfLoader{
+func newCliRegistryConfLoader() cliRegistryHelper {
+	return cliRegistryHelper{
 		cfg: config.LoadDefaultConfigFile(os.Stderr),
 		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 {
 	if err != nil {
 		return nil, err
 		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
 	var registryCreds []containerinstance.ImageRegistryCredential
 	for name, oneCred := range allCreds {
 	for name, oneCred := range allCreds {
 		parsedURL, err := url.Parse(name)
 		parsedURL, err := url.Parse(name)
@@ -107,3 +113,67 @@ func getRegistryCredentials(project compose.Project, registryLoader registryConf
 	}
 	}
 	return registryCreds, nil
 	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
 package convert
 
 
 import (
 import (
+	"errors"
 	"strconv"
 	"strconv"
 	"testing"
 	"testing"
 
 
@@ -30,12 +31,13 @@ import (
 )
 )
 
 
 const getAllCredentials = "getAllRegistryCredentials"
 const getAllCredentials = "getAllRegistryCredentials"
+const autoLoginAcr = "autoLoginAcr"
 
 
 func TestHubPrivateImage(t *testing.T) {
 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.NilError(t, err)
 	assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
 	assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
 		{
 		{
@@ -47,10 +49,10 @@ func TestHubPrivateImage(t *testing.T) {
 }
 }
 
 
 func TestRegistryNameWithoutProtocol(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.NilError(t, err)
 	assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
 	assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
 		{
 		{
@@ -62,19 +64,19 @@ func TestRegistryNameWithoutProtocol(t *testing.T) {
 }
 }
 
 
 func TestInvalidCredentials(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.NilError(t, err)
 	assert.Equal(t, len(creds), 0)
 	assert.Equal(t, len(creds), 0)
 }
 }
 
 
 func TestImageWithDotInName(t *testing.T) {
 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.NilError(t, err)
 	assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
 	assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
 		{
 		{
@@ -86,10 +88,11 @@ func TestImageWithDotInName(t *testing.T) {
 }
 }
 
 
 func TestAcrPrivateImage(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.NilError(t, err)
 	assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
 	assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
 		{
 		{
@@ -101,12 +104,13 @@ func TestAcrPrivateImage(t *testing.T) {
 }
 }
 
 
 func TestAcrPrivateImageLinux(t *testing.T) {
 func TestAcrPrivateImageLinux(t *testing.T) {
-	loader := &MockRegistryLoader{}
+	registryHelper := &MockRegistryHelper{}
 	token := tokenCreds("123456")
 	token := tokenCreds("123456")
 	token.Username = tokenUsername
 	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.NilError(t, err)
 	assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
 	assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
 		{
 		{
@@ -118,14 +122,15 @@ func TestAcrPrivateImageLinux(t *testing.T) {
 }
 }
 
 
 func TestNoMoreRegistriesThanImages(t *testing.T) {
 func TestNoMoreRegistriesThanImages(t *testing.T) {
-	loader := &MockRegistryLoader{}
+	registryHelper := &MockRegistryHelper{}
 	configs := map[string]cliconfigtypes.AuthConfig{
 	configs := map[string]cliconfigtypes.AuthConfig{
 		"https://mycontainerregistrygta.azurecr.io": tokenCreds("123456"),
 		"https://mycontainerregistrygta.azurecr.io": tokenCreds("123456"),
 		"https://index.docker.io":                   userPwdCreds("toto", "pwd"),
 		"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.NilError(t, err)
 	assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
 	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.NilError(t, err)
 	assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
 	assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
 		{
 		{
@@ -147,7 +152,7 @@ func TestNoMoreRegistriesThanImages(t *testing.T) {
 }
 }
 
 
 func TestHubAndSeveralACRRegistries(t *testing.T) {
 func TestHubAndSeveralACRRegistries(t *testing.T) {
-	loader := &MockRegistryLoader{}
+	registryHelper := &MockRegistryHelper{}
 	configs := map[string]cliconfigtypes.AuthConfig{
 	configs := map[string]cliconfigtypes.AuthConfig{
 		"https://mycontainerregistry1.azurecr.io": tokenCreds("123456"),
 		"https://mycontainerregistry1.azurecr.io": tokenCreds("123456"),
 		"https://mycontainerregistry2.azurecr.io": tokenCreds("456789"),
 		"https://mycontainerregistry2.azurecr.io": tokenCreds("456789"),
@@ -155,9 +160,11 @@ func TestHubAndSeveralACRRegistries(t *testing.T) {
 		"https://index.docker.io":                 userPwdCreds("toto", "pwd"),
 		"https://index.docker.io":                 userPwdCreds("toto", "pwd"),
 		"https://other.registry.io":               userPwdCreds("user", "password"),
 		"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.NilError(t, err)
 
 
 	assert.Assert(t, is.Contains(creds, containerinstance.ImageRegistryCredential{
 	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 {
 func composeServices(images ...string) types.Project {
 	var services []types.ServiceConfig
 	var services []types.ServiceConfig
 	for index, name := range images {
 	for index, name := range images {
@@ -210,11 +246,16 @@ func tokenCreds(token string) cliconfigtypes.AuthConfig {
 	}
 	}
 }
 }
 
 
-type MockRegistryLoader struct {
+type MockRegistryHelper struct {
 	mock.Mock
 	mock.Mock
 }
 }
 
 
-func (s *MockRegistryLoader) getAllRegistryCredentials() (map[string]cliconfigtypes.AuthConfig, error) {
+func (s *MockRegistryHelper) getAllRegistryCredentials() (map[string]cliconfigtypes.AuthConfig, error) {
 	args := s.Called()
 	args := s.Called()
 	return args.Get(0).(map[string]cliconfigtypes.AuthConfig), args.Error(1)
 	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
 	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
 // GetValidToken returns an access token. Refresh token if needed
 func (login *AzureLoginService) GetValidToken() (oauth2.Token, error) {
 func (login *AzureLoginService) GetValidToken() (oauth2.Token, error) {
 	loginInfo, err := login.tokenStore.readToken()
 	loginInfo, err := login.tokenStore.readToken()