Explorar el Código

Merge pull request #439 from docker/acr_auto_login

ACR autologin
Guillaume Tardif hace 5 años
padre
commit
036190bd5e
Se han modificado 3 ficheros con 168 adiciones y 48 borrados
  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
 
 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()