Skip to content

Commit

Permalink
feat(hatchery:swarm): login on private registry (#5908)
Browse files Browse the repository at this point in the history
  • Loading branch information
richardlt committed Aug 27, 2021
1 parent 419b91d commit c7f3334
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 11 deletions.
3 changes: 3 additions & 0 deletions engine/config.go
Expand Up @@ -123,6 +123,9 @@ func configBootstrap(args []string) Configuration {
}
conf.Hatchery.Swarm.Name = "cds-hatchery-swarm-" + namesgenerator.GetRandomNameCDS(0)
conf.Hatchery.Swarm.HTTP.Port = 8086
conf.Hatchery.Swarm.RegistryCredentials = []swarm.RegistryCredential{{
Domain: "docker.io",
}}
case sdk.TypeHatchery + ":vsphere":
conf.Hatchery.VSphere = &vsphere.HatcheryConfiguration{}
defaults.SetDefaults(conf.Hatchery.VSphere)
Expand Down
68 changes: 58 additions & 10 deletions engine/hatchery/swarm/swarm_util_pull.go
Expand Up @@ -3,16 +3,18 @@ package swarm
import (
"bytes"
"encoding/base64"
"fmt"
"encoding/json"
"io"
"net/url"
"regexp"
"time"

"github.com/ovh/cds/sdk"
"github.com/rockbears/log"

"github.com/docker/distribution/reference"
types "github.com/docker/docker/api/types"
"github.com/rockbears/log"
context "golang.org/x/net/context"

"github.com/ovh/cds/sdk"
)

func (h *HatcherySwarm) pullImage(dockerClient *dockerClient, img string, timeout time.Duration, model sdk.Model) error {
Expand All @@ -23,23 +25,69 @@ func (h *HatcherySwarm) pullImage(dockerClient *dockerClient, img string, timeou
defer cancel()

//Pull the worker image
opts := types.ImageCreateOptions{}
var authConfig *types.AuthConfig
if model.ModelDocker.Private {
registry := "index.docker.io"
if model.ModelDocker.Registry != "" {
urlParsed, errParsed := url.Parse(model.ModelDocker.Registry)
if errParsed != nil {
return sdk.WrapError(errParsed, "cannot parse registry url %s", registry)
urlParsed, err := url.Parse(model.ModelDocker.Registry)
if err != nil {
return sdk.WrapError(err, "cannot parse registry url %q", registry)
}
if urlParsed.Host == "" {
registry = urlParsed.Path
} else {
registry = urlParsed.Host
}
}
auth := fmt.Sprintf(`{"username": "%s", "password": "%s", "serveraddress": "%s"}`, model.ModelDocker.Username, model.ModelDocker.Password, registry)
opts.RegistryAuth = base64.StdEncoding.EncodeToString([]byte(auth))
authConfig = &types.AuthConfig{
Username: model.ModelDocker.Username,
Password: model.ModelDocker.Password,
ServerAddress: registry,
}
} else {
ref, err := reference.ParseNormalizedNamed(img)
if err != nil {
return sdk.WithStack(err)
}
domain := reference.Domain(ref)
var credentials *RegistryCredential
// Check if credentials match current domain
for i := range h.Config.RegistryCredentials {
if h.Config.RegistryCredentials[i].Domain == domain {
credentials = &h.Config.RegistryCredentials[i]
break
}
}
if credentials == nil {
// Check if regex credentials match current domain
for i := range h.Config.RegistryCredentials {
reg := regexp.MustCompile(h.Config.RegistryCredentials[i].Domain)
if reg.MatchString(domain) {
credentials = &h.Config.RegistryCredentials[i]
break
}
}
}
if credentials != nil {
authConfig = &types.AuthConfig{
Username: credentials.Username,
Password: credentials.Password,
ServerAddress: domain,
}
log.Debug(context.TODO(), "hatchery> swarm> pullImage> Found credentials %q to pull image %q", credentials.Domain, img)
}
}

opts := types.ImageCreateOptions{}
if authConfig != nil {
config, err := json.Marshal(authConfig)
if err != nil {
return sdk.WithStack(err)
}
opts.RegistryAuth = base64.StdEncoding.EncodeToString(config)
log.Debug(context.TODO(), "hatchery> swarm> pullImage> pulling image %q on %q with login on %q", img, dockerClient.name, authConfig.ServerAddress)
}

res, err := dockerClient.ImageCreate(ctx, img, opts)
if err != nil {
log.Warn(ctx, "hatchery> swarm> pullImage> Unable to pull image %s on %s: %s", img, dockerClient.name, err)
Expand Down
85 changes: 85 additions & 0 deletions engine/hatchery/swarm/swarm_util_pull_test.go
@@ -0,0 +1,85 @@
package swarm

import (
"encoding/base64"
"encoding/json"
"net/http"
"testing"
"time"

"github.com/docker/docker/api/types"
"github.com/stretchr/testify/require"
"gopkg.in/h2non/gock.v1"

"github.com/ovh/cds/sdk"
)

func Test_pullImage(t *testing.T) {
t.Cleanup(gock.Off)

h := InitTestHatcherySwarm(t)

gock.New("https://lolcat.host").Post("/images/create").Times(5).AddMatcher(func(r *http.Request, rr *gock.Request) (bool, error) {
values := r.URL.Query()

// Call 1
if values.Get("fromImage") == "my-registry.lolcat.host/my-image-1" && values.Get("tag") == "my-tag" {
return true, nil
}
// Call 2
if values.Get("fromImage") == "my-image-2" && values.Get("tag") == "my-tag" {
return true, nil
}

buf, err := base64.StdEncoding.DecodeString(r.Header.Get("X-Registry-Auth"))
require.NoError(t, err)
var auth types.AuthConfig
require.NoError(t, json.Unmarshal(buf, &auth))

t.Log("Auth config", auth)

// Call 3
if values.Get("fromImage") == "my-first-registry.lolcat.host/my-image-3" && values.Get("tag") == "my-tag" &&
auth.Username == "my-user" && auth.Password == "my-pass-1" && auth.ServerAddress == "my-first-registry.lolcat.host" {
return true, nil
}
// Call 4
if values.Get("fromImage") == "my-second-registry.lolcat.host/my-image-4" && values.Get("tag") == "my-tag" &&
auth.Username == "my-user" && auth.Password == "my-pass-2" && auth.ServerAddress == "my-second-registry.lolcat.host" {
return true, nil
}
// Call 5
if values.Get("fromImage") == "my-image-5" && values.Get("tag") == "my-tag" &&
auth.Username == "my-user" && auth.Password == "my-pass" && auth.ServerAddress == "docker.io" {
return true, nil
}
return false, nil
}).Reply(http.StatusOK)

require.NoError(t, h.pullImage(h.dockerClients["default"], "my-registry.lolcat.host/my-image-1:my-tag", time.Minute, sdk.Model{}))
require.NoError(t, h.pullImage(h.dockerClients["default"], "my-image-2:my-tag", time.Minute, sdk.Model{}))

h.Config.RegistryCredentials = []RegistryCredential{
{
Domain: "docker.io",
Username: "my-user",
Password: "my-pass",
},
{
Domain: "my-first-registry.lolcat.host",
Username: "my-user",
Password: "my-pass-1",
},
{
Domain: "^*.lolcat.host$",
Username: "my-user",
Password: "my-pass-2",
},
}

require.NoError(t, h.pullImage(h.dockerClients["default"], "my-first-registry.lolcat.host/my-image-3:my-tag", time.Minute, sdk.Model{}))
require.NoError(t, h.pullImage(h.dockerClients["default"], "my-second-registry.lolcat.host/my-image-4:my-tag", time.Minute, sdk.Model{}))
require.NoError(t, h.pullImage(h.dockerClients["default"], "my-image-5:my-tag", time.Minute, sdk.Model{}))

require.True(t, gock.IsDone())
}
8 changes: 8 additions & 0 deletions engine/hatchery/swarm/types.go
Expand Up @@ -31,6 +31,8 @@ type HatcheryConfiguration struct {
NetworkEnableIPv6 bool `mapstructure:"networkEnableIPv6" toml:"networkEnableIPv6" default:"false" commented:"false" comment:"if true: hatchery creates private network between services with ipv6 enabled" json:"networkEnableIPv6"`

DockerEngines map[string]DockerEngineConfiguration `mapstructure:"dockerEngines" toml:"dockerEngines" comment:"List of Docker Engines" json:"dockerEngines,omitempty"`

RegistryCredentials []RegistryCredential `mapstructure:"registryCredentials" toml:"registryCredentials" commented:"true" comment:"List of Docker registry credentials" json:"-"`
}

// HatcherySwarm is a hatchery which can be connected to a remote to a docker remote api
Expand All @@ -57,3 +59,9 @@ type DockerEngineConfiguration struct {
APIVersion string `mapstructure:"APIVersion" toml:"APIVersion" comment:"DOCKER_API_VERSION" json:"APIVersion"` // DOCKER_API_VERSION
MaxContainers int `mapstructure:"maxContainers" toml:"maxContainers" default:"10" commented:"false" comment:"Max Containers on Host managed by this Hatchery" json:"maxContainers"`
}

type RegistryCredential struct {
Domain string `mapstructure:"domain" default:"docker.io" commented:"true" toml:"domain" json:"-"`
Username string `mapstructure:"username" commented:"true" toml:"username" json:"-"`
Password string `mapstructure:"password" commented:"true" toml:"password" json:"-"`
}
2 changes: 1 addition & 1 deletion engine/service/types.go
Expand Up @@ -53,7 +53,7 @@ type HatcheryCommonConfiguration struct {
MaxConcurrentRegistering int `toml:"maxConcurrentRegistering" default:"2" comment:"Maximum allowed simultaneous workers registering. -1 to disable registering on this hatchery" json:"maxConcurrentRegistering"`
RegisterFrequency int `toml:"registerFrequency" default:"60" comment:"Check if some worker model have to be registered each n Seconds" json:"registerFrequency"`
Region string `toml:"region" default:"" comment:"region of this hatchery - optional. With a free text as 'myregion', user can set a prerequisite 'region' with value 'myregion' on CDS Job" json:"region"`
IgnoreJobWithNoRegion bool `toml:"ignoreJobWithNoRegion" default:"false" comment:"Ignore job without a region prerequisite if ignoreJobWithNoRegion=true"`
IgnoreJobWithNoRegion bool `toml:"ignoreJobWithNoRegion" default:"false" comment:"Ignore job without a region prerequisite if ignoreJobWithNoRegion=true" json:"ignoreJobWithNoRegion"`
WorkerAPIHTTP struct {
URL string `toml:"url" default:"http://localhost:8081" commented:"true" comment:"CDS API URL for worker, let empty or commented to use the same URL that is used by the Hatchery" json:"url"`
Insecure bool `toml:"insecure" default:"false" commented:"true" comment:"sslInsecureSkipVerify, set to true if you use a self-signed SSL on CDS API" json:"insecure"`
Expand Down

0 comments on commit c7f3334

Please sign in to comment.