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

View File

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

View File

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