Răsfoiți Sursa

Merge pull request #165 from docker/azure_private_images

Allow pulling private images from hub or other registries
Guillaume Tardif 5 ani în urmă
părinte
comite
e77513c1c8

+ 10 - 3
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: &registryCreds,
 		},
 	}
 

+ 88 - 0
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
+}

+ 192 - 0
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)
+}

+ 3 - 0
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

+ 9 - 0
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=