Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(hatchery:swarm): login on private registry #5908

Merged
merged 2 commits into from
Aug 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions engine/config.go
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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