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
This commit is contained in:
Guillaume Tardif 2020-06-02 15:29:16 +02:00
parent 3a2e16dc7a
commit 11f1c057dc
5 changed files with 302 additions and 3 deletions

View File

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

View File

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

View File

@ -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
go.mod
View File

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

9
go.sum
View File

@ -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=