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.

This commit is contained in:
Guillaume Tardif 2020-08-07 16:33:20 +02:00
parent 7326234e25
commit 63fd8f2fad
3 changed files with 168 additions and 48 deletions

View File

@ -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" tokenUsername = "00000000-0000-0000-0000-000000000000"
dockerHub = "index.docker.io" 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 { func newCliRegistryConfLoader() cliRegistryHelper {
return cliRegistryConfLoader{ return cliRegistryHelper{
cfg: config.LoadDefaultConfigFile(os.Stderr), cfg: config.LoadDefaultConfigFile(os.Stderr),
} }
} }
func getRegistryCredentials(project compose.Project, registryLoader registryConfLoader) ([]containerinstance.ImageRegistryCredential, error) { func getRegistryCredentials(project compose.Project, helper registryHelper) ([]containerinstance.ImageRegistryCredential, error) {
allCreds, err := registryLoader.getAllRegistryCredentials() 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()
}

View File

@ -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{} registryHelper := &MockRegistryHelper{}
loader.On(getAllCredentials).Return(registry("https://index.docker.io", userPwdCreds("toto", "pwd")), nil) 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{} registryHelper := &MockRegistryHelper{}
loader.On(getAllCredentials).Return(registry("index.docker.io", userPwdCreds("toto", "pwd")), nil) 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{} registryHelper := &MockRegistryHelper{}
loader.On(getAllCredentials).Return(registry("18.195.159.6:444", userPwdCreds("toto", "pwd")), nil) 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{} registryHelper := &MockRegistryHelper{}
loader.On(getAllCredentials).Return(registry("index.docker.io", userPwdCreds("toto", "pwd")), nil) 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{} registryHelper := &MockRegistryHelper{}
loader.On(getAllCredentials).Return(registry("https://mycontainerregistrygta.azurecr.io", tokenCreds("123456")), nil) 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)
}

View File

@ -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()