From 11f1c057dc9a90949ffb5c7f51360a88ddd85f39 Mon Sep 17 00:00:00 2001 From: Guillaume Tardif Date: Tue, 2 Jun 2020 15:29:16 +0200 Subject: [PATCH] Allow pulling private images from hub or other registries, as long as the user is logged in against the registry(ies). Registry creds are passed along with the ACI payload (cf https://docs.microsoft.com/en-us/azure/container-registry/container-registry-auth-aci). Manually tested against hub & ACR private repo --- azure/convert/convert.go | 13 +- azure/convert/registrycredentials.go | 88 ++++++++++ azure/convert/registrycredentials_test.go | 192 ++++++++++++++++++++++ go.mod | 3 + go.sum | 9 + 5 files changed, 302 insertions(+), 3 deletions(-) create mode 100644 azure/convert/registrycredentials.go create mode 100644 azure/convert/registrycredentials_test.go diff --git a/azure/convert/convert.go b/azure/convert/convert.go index 1f4523ac..22388087 100644 --- a/azure/convert/convert.go +++ b/azure/convert/convert.go @@ -42,14 +42,21 @@ func ToContainerGroup(aciContext store.AciContext, p compose.Project) (container } else { volumes = &allVolumes } + + registryCreds, err := getRegistryCredentials(p, newCliRegistryConfLoader()) + if err != nil { + return containerinstance.ContainerGroup{}, err + } + var containers []containerinstance.Container groupDefinition := containerinstance.ContainerGroup{ Name: &containerGroupName, Location: &aciContext.Location, ContainerGroupProperties: &containerinstance.ContainerGroupProperties{ - OsType: containerinstance.Linux, - Containers: &containers, - Volumes: volumes, + OsType: containerinstance.Linux, + Containers: &containers, + Volumes: volumes, + ImageRegistryCredentials: ®istryCreds, }, } diff --git a/azure/convert/registrycredentials.go b/azure/convert/registrycredentials.go new file mode 100644 index 00000000..32180698 --- /dev/null +++ b/azure/convert/registrycredentials.go @@ -0,0 +1,88 @@ +package convert + +import ( + "net/url" + "os" + "strings" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/containerinstance/mgmt/containerinstance" + "github.com/Azure/go-autorest/autorest/to" + "github.com/docker/cli/cli/config" + "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/cli/config/types" + + "github.com/docker/api/compose" +) + +// 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" +) + +type registryConfLoader interface { + getAllRegistryCredentials() (map[string]types.AuthConfig, error) +} + +type cliRegistryConfLoader struct { + cfg *configfile.ConfigFile +} + +func (c cliRegistryConfLoader) getAllRegistryCredentials() (map[string]types.AuthConfig, error) { + return c.cfg.GetAllCredentials() +} + +func newCliRegistryConfLoader() cliRegistryConfLoader { + return cliRegistryConfLoader{ + cfg: config.LoadDefaultConfigFile(os.Stderr), + } +} + +func getRegistryCredentials(project compose.Project, registryLoader registryConfLoader) ([]containerinstance.ImageRegistryCredential, error) { + allCreds, err := registryLoader.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) + if err != nil { + return nil, err + } + + hostname := parsedURL.Host + if hostname == "" { + hostname = parsedURL.Path + } + if _, ok := usedRegistries[hostname]; ok { + if oneCred.Username != "" { + aciCredential := containerinstance.ImageRegistryCredential{ + Server: to.StringPtr(hostname), + Password: to.StringPtr(oneCred.Password), + Username: to.StringPtr(oneCred.Username), + } + registryCreds = append(registryCreds, aciCredential) + } else if oneCred.IdentityToken != "" { + aciCredential := containerinstance.ImageRegistryCredential{ + Server: to.StringPtr(hostname), + Password: to.StringPtr(oneCred.IdentityToken), + Username: to.StringPtr(tokenUsername), + } + registryCreds = append(registryCreds, aciCredential) + } + } + } + return registryCreds, nil +} diff --git a/azure/convert/registrycredentials_test.go b/azure/convert/registrycredentials_test.go new file mode 100644 index 00000000..8a742691 --- /dev/null +++ b/azure/convert/registrycredentials_test.go @@ -0,0 +1,192 @@ +package convert + +import ( + "strconv" + + "github.com/Azure/go-autorest/autorest/to" + "github.com/compose-spec/compose-go/types" + cliconfigtypes "github.com/docker/cli/cli/config/types" + + "github.com/docker/api/compose" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/containerinstance/mgmt/containerinstance" + + "testing" + + . "github.com/onsi/gomega" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +const getAllCredentials = "getAllRegistryCredentials" + +type RegistryConvertTestSuite struct { + suite.Suite + loader *MockRegistryLoader +} + +func (suite *RegistryConvertTestSuite) BeforeTest(suiteName, testName string) { + suite.loader = &MockRegistryLoader{} +} + +func (suite *RegistryConvertTestSuite) TestHubPrivateImage() { + suite.loader.On(getAllCredentials).Return(registry("https://index.docker.io", userPwdCreds("toto", "pwd")), nil) + + creds, err := getRegistryCredentials(composeServices("gtardif/privateimg"), suite.loader) + Expect(err).To(BeNil()) + Expect(creds).To(Equal([]containerinstance.ImageRegistryCredential{ + { + Server: to.StringPtr(dockerHub), + Username: to.StringPtr("toto"), + Password: to.StringPtr("pwd"), + }, + })) +} + +func (suite *RegistryConvertTestSuite) TestRegistryNameWithoutProtocol() { + suite.loader.On(getAllCredentials).Return(registry("index.docker.io", userPwdCreds("toto", "pwd")), nil) + + creds, err := getRegistryCredentials(composeServices("gtardif/privateimg"), suite.loader) + Expect(err).To(BeNil()) + Expect(creds).To(Equal([]containerinstance.ImageRegistryCredential{ + { + Server: to.StringPtr(dockerHub), + Username: to.StringPtr("toto"), + Password: to.StringPtr("pwd"), + }, + })) +} + +func (suite *RegistryConvertTestSuite) TestImageWithDotInName() { + suite.loader.On(getAllCredentials).Return(registry("index.docker.io", userPwdCreds("toto", "pwd")), nil) + + creds, err := getRegistryCredentials(composeServices("my.image"), suite.loader) + Expect(err).To(BeNil()) + Expect(creds).To(Equal([]containerinstance.ImageRegistryCredential{ + { + Server: to.StringPtr(dockerHub), + Username: to.StringPtr("toto"), + Password: to.StringPtr("pwd"), + }, + })) +} + +func (suite *RegistryConvertTestSuite) TestAcrPrivateImage() { + suite.loader.On(getAllCredentials).Return(registry("https://mycontainerregistrygta.azurecr.io", tokenCreds("123456")), nil) + + creds, err := getRegistryCredentials(composeServices("mycontainerregistrygta.azurecr.io/privateimg"), suite.loader) + Expect(err).To(BeNil()) + Expect(creds).To(Equal([]containerinstance.ImageRegistryCredential{ + { + Server: to.StringPtr("mycontainerregistrygta.azurecr.io"), + Username: to.StringPtr(tokenUsername), + Password: to.StringPtr("123456"), + }, + })) +} + +func (suite *RegistryConvertTestSuite) TestNoMoreRegistriesThanImages() { + configs := map[string]cliconfigtypes.AuthConfig{ + "https://mycontainerregistrygta.azurecr.io": tokenCreds("123456"), + "https://index.docker.io": userPwdCreds("toto", "pwd"), + } + suite.loader.On(getAllCredentials).Return(configs, nil) + + creds, err := getRegistryCredentials(composeServices("mycontainerregistrygta.azurecr.io/privateimg"), suite.loader) + Expect(err).To(BeNil()) + Expect(creds).To(Equal([]containerinstance.ImageRegistryCredential{ + { + Server: to.StringPtr("mycontainerregistrygta.azurecr.io"), + Username: to.StringPtr(tokenUsername), + Password: to.StringPtr("123456"), + }, + })) + + creds, err = getRegistryCredentials(composeServices("someuser/privateimg"), suite.loader) + Expect(err).To(BeNil()) + Expect(creds).To(Equal([]containerinstance.ImageRegistryCredential{ + { + Server: to.StringPtr(dockerHub), + Username: to.StringPtr("toto"), + Password: to.StringPtr("pwd"), + }, + })) +} + +func (suite *RegistryConvertTestSuite) TestHubAndSeveralACRRegistries() { + configs := map[string]cliconfigtypes.AuthConfig{ + "https://mycontainerregistry1.azurecr.io": tokenCreds("123456"), + "https://mycontainerregistry2.azurecr.io": tokenCreds("456789"), + "https://mycontainerregistry3.azurecr.io": tokenCreds("123456789"), + "https://index.docker.io": userPwdCreds("toto", "pwd"), + "https://other.registry.io": userPwdCreds("user", "password"), + } + suite.loader.On(getAllCredentials).Return(configs, nil) + + creds, err := getRegistryCredentials(composeServices("mycontainerregistry1.azurecr.io/privateimg", "someuser/privateImg2", "mycontainerregistry2.azurecr.io/privateimg"), suite.loader) + Expect(err).To(BeNil()) + Expect(creds).To(ContainElement(containerinstance.ImageRegistryCredential{ + Server: to.StringPtr("mycontainerregistry1.azurecr.io"), + Username: to.StringPtr(tokenUsername), + Password: to.StringPtr("123456"), + })) + Expect(creds).To(ContainElement(containerinstance.ImageRegistryCredential{ + Server: to.StringPtr("mycontainerregistry2.azurecr.io"), + Username: to.StringPtr(tokenUsername), + Password: to.StringPtr("456789"), + })) + Expect(creds).To(ContainElement(containerinstance.ImageRegistryCredential{ + Server: to.StringPtr(dockerHub), + Username: to.StringPtr("toto"), + Password: to.StringPtr("pwd"), + })) +} + +func composeServices(images ...string) compose.Project { + var services []types.ServiceConfig + for index, name := range images { + service := types.ServiceConfig{ + Name: "service" + strconv.Itoa(index), + Image: name, + } + services = append(services, service) + } + return compose.Project{ + Config: types.Config{ + Services: services, + }, + } +} + +func registry(host string, configregistryData cliconfigtypes.AuthConfig) map[string]cliconfigtypes.AuthConfig { + return map[string]cliconfigtypes.AuthConfig{ + host: configregistryData, + } +} + +func userPwdCreds(user string, password string) cliconfigtypes.AuthConfig { + return cliconfigtypes.AuthConfig{ + Username: user, + Password: password, + } +} + +func tokenCreds(token string) cliconfigtypes.AuthConfig { + return cliconfigtypes.AuthConfig{ + IdentityToken: token, + } +} + +func TestRegistryConvertTestSuite(t *testing.T) { + RegisterTestingT(t) + suite.Run(t, new(RegistryConvertTestSuite)) +} + +type MockRegistryLoader struct { + mock.Mock +} + +func (s *MockRegistryLoader) getAllRegistryCredentials() (map[string]cliconfigtypes.AuthConfig, error) { + args := s.Called() + return args.Get(0).(map[string]cliconfigtypes.AuthConfig), args.Error(1) +} diff --git a/go.mod b/go.mod index 3f7c826b..fdeb5e6e 100644 --- a/go.mod +++ b/go.mod @@ -17,8 +17,10 @@ require ( github.com/compose-spec/compose-go v0.0.0-20200423124427-63dcf8c22cae github.com/containerd/console v1.0.0 github.com/containerd/containerd v1.3.4 // indirect + github.com/docker/cli v0.0.0-20200528204125-dd360c7c0de8 github.com/docker/distribution v2.7.1+incompatible // indirect github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible + github.com/docker/docker-credential-helpers v0.6.3 // indirect github.com/docker/go-connections v0.4.0 github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect github.com/gobwas/pool v0.2.0 // indirect @@ -31,6 +33,7 @@ require ( github.com/onsi/gomega v1.9.0 github.com/opencontainers/go-digest v1.0.0-rc1 github.com/opencontainers/image-spec v1.0.1 // indirect + github.com/opencontainers/runc v0.1.1 // indirect github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.5.1 // indirect github.com/robpike/filter v0.0.0-20150108201509-2984852a2183 diff --git a/go.sum b/go.sum index ea77a4a4..316aa1ae 100644 --- a/go.sum +++ b/go.sum @@ -76,10 +76,17 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4= github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/docker/cli v0.0.0-20200303162255-7d407207c304 h1:A7SYzidcyuQ/yS4wezWGYeUioUFJQk8HYWY9aMYTF4I= +github.com/docker/cli v0.0.0-20200303162255-7d407207c304/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v0.0.0-20200528204125-dd360c7c0de8 h1:JRquW4uqIU+eSilDhuo9X9QFX4NEmGj5B1x97ZA8djM= +github.com/docker/cli v0.0.0-20200528204125-dd360c7c0de8/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v17.12.1-ce-rc2+incompatible h1:ESUycEAqvFuLglAHkUW66rCc2djYtd3i1x231svLq9o= github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible h1:G2hY8RD7jB9QaSmcb8mYEIg8QbEvVAB7se8+lXHZHfg= github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ= +github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= @@ -189,6 +196,8 @@ github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2i github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=