diff --git a/azure/convert/convert.go b/azure/convert/convert.go index 1f4523acf..223880879 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 000000000..321806981 --- /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 000000000..8a7426917 --- /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 6fe82548c..d20529a44 100644 --- a/go.mod +++ b/go.mod @@ -18,8 +18,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 @@ -33,6 +35,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 1465ba723..6400a6c43 100644 --- a/go.sum +++ b/go.sum @@ -80,10 +80,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= @@ -207,6 +214,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=