Merge pull request #439 from docker/acr_auto_login

ACR autologin
This commit is contained in:
Guillaume Tardif 2020-08-10 11:36:28 +02:00 committed by GitHub
commit 036190bd5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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()