diff --git a/internal/config/config.go b/internal/config/config.go index e97f18a..b543da3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -59,15 +59,31 @@ const ( RuntimeTypePod RuntimeType = "pod" ) +type RegistryAuthType string + +const ( + RegistryAuthTypeDefault RegistryAuthType = "default" +) + +type RegistryAuth struct { + Type RegistryAuthType `yaml:"type"` + + // default auth + Username Value `yaml:"username"` + Password Value `yaml:"password"` +} + type Runtime struct { - Name string `yaml:"name"` - Type RuntimeType `yaml:"type,omitempty"` - Arch common.Arch `yaml:"arch,omitempty"` - Containers []*Container `yaml:"containers,omitempty"` + Name string `yaml:"name"` + Type RuntimeType `yaml:"type,omitempty"` + Auth *RegistryAuth `yaml:"auth"` + Arch common.Arch `yaml:"arch,omitempty"` + Containers []*Container `yaml:"containers,omitempty"` } type Container struct { Image string `yaml:"image,omitempty"` + Auth *RegistryAuth `yaml:"auth"` Environment map[string]Value `yaml:"environment,omitempty"` User string `yaml:"user"` Privileged bool `yaml:"privileged"` @@ -539,6 +555,22 @@ func ParseConfig(configData []byte) (*Config, error) { } } + // Set auth type to default if not specified + for _, runtime := range config.Runtimes { + if runtime.Auth != nil { + if runtime.Auth.Type == "" { + runtime.Auth.Type = RegistryAuthTypeDefault + } + } + for _, container := range runtime.Containers { + if container.Auth != nil { + if container.Auth.Type == "" { + container.Auth.Type = RegistryAuthTypeDefault + } + } + } + } + return &config, checkConfig(&config) } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 77d17cc..229ea38 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -134,8 +134,16 @@ func TestParseOutput(t *testing.T) { runtimes: runtime01: type: pod + auth: + username: username + password: + from_variable: password containers: - image: image01 + auth: + username: + from_variable: username2 + password: password2 environment: ENV01: ENV01 ENVFROMVARIABLE01: @@ -179,10 +187,20 @@ func TestParseOutput(t *testing.T) { "runtime01": &Runtime{ Name: "runtime01", Type: "pod", + Auth: &RegistryAuth{ + Type: RegistryAuthTypeDefault, + Username: Value{Type: ValueTypeString, Value: "username"}, + Password: Value{Type: ValueTypeFromVariable, Value: "password"}, + }, Arch: "", Containers: []*Container{ &Container{ Image: "image01", + Auth: &RegistryAuth{ + Type: RegistryAuthTypeDefault, + Username: Value{Type: ValueTypeFromVariable, Value: "username2"}, + Password: Value{Type: ValueTypeString, Value: "password2"}, + }, Environment: map[string]Value{ "ENV01": Value{Type: ValueTypeString, Value: "ENV01"}, "ENVFROMVARIABLE01": Value{Type: ValueTypeFromVariable, Value: "variable01"}, diff --git a/internal/runconfig/runconfig.go b/internal/runconfig/runconfig.go index 62292b2..ecef6bd 100644 --- a/internal/runconfig/runconfig.go +++ b/internal/runconfig/runconfig.go @@ -38,6 +38,24 @@ func genRuntime(c *config.Config, runtimeName string, variables map[string]strin Privileged: cc.Privileged, Entrypoint: cc.Entrypoint, } + + // Set container auth + if cc.Auth != nil { + container.Auth = &rstypes.RegistryAuth{ + Type: rstypes.RegistryAuthType(cc.Auth.Type), + Username: genValue(cc.Auth.Username, variables), + Password: genValue(cc.Auth.Password, variables), + } + } + // if container auth is nil use runtime auth + if container.Auth == nil && ce.Auth != nil { + container.Auth = &rstypes.RegistryAuth{ + Type: rstypes.RegistryAuthType(ce.Auth.Type), + Username: genValue(ce.Auth.Username, variables), + Password: genValue(ce.Auth.Password, variables), + } + } + containers = append(containers, container) } diff --git a/internal/runconfig/runconfig_test.go b/internal/runconfig/runconfig_test.go index 94b9b6a..7a595ab 100644 --- a/internal/runconfig/runconfig_test.go +++ b/internal/runconfig/runconfig_test.go @@ -702,13 +702,23 @@ func TestGenRunConfig(t *testing.T) { "runtime01": &config.Runtime{ Name: "runtime01", Type: "pod", + Auth: &config.RegistryAuth{ + Type: config.RegistryAuthTypeDefault, + Username: config.Value{Type: config.ValueTypeString, Value: "username"}, + Password: config.Value{Type: config.ValueTypeFromVariable, Value: "password"}, + }, Arch: "", Containers: []*config.Container{ &config.Container{ Image: "image01", - Environment: map[string]config.EnvVar{ - "ENV01": config.EnvVar{Type: config.EnvVarTypeString, Value: "ENV01"}, - "ENVFROMVARIABLE01": config.EnvVar{Type: config.EnvVarTypeFromVariable, Value: "variable01"}, + Auth: &config.RegistryAuth{ + Type: config.RegistryAuthTypeDefault, + Username: config.Value{Type: config.ValueTypeFromVariable, Value: "registry_username"}, + Password: config.Value{Type: config.ValueTypeString, Value: "password2"}, + }, + Environment: map[string]config.Value{ + "ENV01": config.Value{Type: config.ValueTypeString, Value: "ENV01"}, + "ENVFROMVARIABLE01": config.Value{Type: config.ValueTypeFromVariable, Value: "variable01"}, }, User: "", }, @@ -719,9 +729,9 @@ func TestGenRunConfig(t *testing.T) { "task01": &config.Task{ Name: "task01", Runtime: "runtime01", - Environment: map[string]config.EnvVar{ - "ENV01": config.EnvVar{Type: config.EnvVarTypeString, Value: "ENV01"}, - "ENVFROMVARIABLE01": config.EnvVar{Type: config.EnvVarTypeFromVariable, Value: "variable01"}, + Environment: map[string]config.Value{ + "ENV01": config.Value{Type: config.ValueTypeString, Value: "ENV01"}, + "ENVFROMVARIABLE01": config.Value{Type: config.ValueTypeFromVariable, Value: "variable01"}, }, WorkingDir: "", Shell: "", @@ -747,9 +757,9 @@ func TestGenRunConfig(t *testing.T) { Name: "command03", }, Command: "command03", - Environment: map[string]config.EnvVar{ - "ENV01": config.EnvVar{Type: config.EnvVarTypeString, Value: "ENV01"}, - "ENVFROMVARIABLE01": config.EnvVar{Type: config.EnvVarTypeFromVariable, Value: "variable01"}, + Environment: map[string]config.Value{ + "ENV01": config.Value{Type: config.ValueTypeString, Value: "ENV01"}, + "ENVFROMVARIABLE01": config.Value{Type: config.ValueTypeFromVariable, Value: "variable01"}, }, }, }, @@ -779,7 +789,8 @@ func TestGenRunConfig(t *testing.T) { }, }, variables: map[string]string{ - "variable01": "VARVALUE01", + "variable01": "VARVALUE01", + "registry_username": "yourregistryusername", }, out: map[string]*rstypes.RunConfigTask{ uuid.New("element01").String(): &rstypes.RunConfigTask{ @@ -789,6 +800,11 @@ func TestGenRunConfig(t *testing.T) { Containers: []*rstypes.Container{ { Image: "image01", + Auth: &rstypes.RegistryAuth{ + Type: rstypes.RegistryAuthTypeDefault, + Username: "yourregistryusername", + Password: "password2", + }, Environment: map[string]string{ "ENV01": "ENV01", "ENVFROMVARIABLE01": "VARVALUE01", @@ -809,6 +825,161 @@ func TestGenRunConfig(t *testing.T) { }, }, }, + { + name: "test runtime auth used for container nil auth", + in: &config.Config{ + Runtimes: map[string]*config.Runtime{ + "runtime01": &config.Runtime{ + Name: "runtime01", + Type: "pod", + Auth: &config.RegistryAuth{ + Type: config.RegistryAuthTypeDefault, + Username: config.Value{Type: config.ValueTypeString, Value: "username"}, + Password: config.Value{Type: config.ValueTypeFromVariable, Value: "password"}, + }, + Arch: "", + Containers: []*config.Container{ + &config.Container{ + Image: "image01", + }, + }, + }, + }, + Tasks: map[string]*config.Task{ + "task01": &config.Task{ + Name: "task01", + Runtime: "runtime01", + Steps: []interface{}{ + &config.RunStep{ + Step: config.Step{ + Type: "run", + Name: "command01", + }, + Command: "command01", + }, + }, + }, + }, + Pipelines: map[string]*config.Pipeline{ + "pipeline01": &config.Pipeline{ + Name: "pipeline01", + Elements: map[string]*config.Element{ + "element01": &config.Element{ + Name: "element01", + Task: "task01", + }, + }, + }, + }, + }, + variables: map[string]string{ + "variable01": "VARVALUE01", + "password": "yourregistrypassword", + }, + out: map[string]*rstypes.RunConfigTask{ + uuid.New("element01").String(): &rstypes.RunConfigTask{ + ID: uuid.New("element01").String(), + Name: "element01", Depends: []*rstypes.RunConfigTaskDepend{}, + Runtime: &rstypes.Runtime{Type: rstypes.RuntimeType("pod"), + Containers: []*rstypes.Container{ + { + Image: "image01", + Auth: &rstypes.RegistryAuth{ + Type: rstypes.RegistryAuthTypeDefault, + Username: "username", + Password: "yourregistrypassword", + }, + Environment: map[string]string{}, + }, + }, + }, + Environment: map[string]string{}, + Steps: []interface{}{ + &rstypes.RunStep{Step: rstypes.Step{Type: "run", Name: "command01"}, Command: "command01", Environment: map[string]string{}}, + }, + }, + }, + }, + { + name: "test runtime auth not used for container with auth", + in: &config.Config{ + Runtimes: map[string]*config.Runtime{ + "runtime01": &config.Runtime{ + Name: "runtime01", + Type: "pod", + Auth: &config.RegistryAuth{ + Type: config.RegistryAuthTypeDefault, + Username: config.Value{Type: config.ValueTypeString, Value: "username"}, + Password: config.Value{Type: config.ValueTypeFromVariable, Value: "password"}, + }, + Arch: "", + Containers: []*config.Container{ + &config.Container{ + Image: "image01", + Auth: &config.RegistryAuth{ + Type: config.RegistryAuthTypeDefault, + Username: config.Value{Type: config.ValueTypeFromVariable, Value: "registry_username"}, + Password: config.Value{Type: config.ValueTypeString, Value: "password2"}, + }, + }, + }, + }, + }, + Tasks: map[string]*config.Task{ + "task01": &config.Task{ + Name: "task01", + Runtime: "runtime01", + Steps: []interface{}{ + &config.RunStep{ + Step: config.Step{ + Type: "run", + Name: "command01", + }, + Command: "command01", + }, + }, + }, + }, + Pipelines: map[string]*config.Pipeline{ + "pipeline01": &config.Pipeline{ + Name: "pipeline01", + Elements: map[string]*config.Element{ + "element01": &config.Element{ + Name: "element01", + Task: "task01", + }, + }, + }, + }, + }, + variables: map[string]string{ + "variable01": "VARVALUE01", + "registry_username": "yourregistryusername", + }, + out: map[string]*rstypes.RunConfigTask{ + uuid.New("element01").String(): &rstypes.RunConfigTask{ + ID: uuid.New("element01").String(), + Name: "element01", Depends: []*rstypes.RunConfigTaskDepend{}, + Runtime: &rstypes.Runtime{Type: rstypes.RuntimeType("pod"), + Containers: []*rstypes.Container{ + { + Image: "image01", + Auth: &rstypes.RegistryAuth{ + Type: rstypes.RegistryAuthTypeDefault, + Username: "yourregistryusername", + Password: "password2", + }, + Environment: map[string]string{}, + }, + }, + }, + Environment: map[string]string{}, + Steps: []interface{}{ + &rstypes.RunStep{Step: rstypes.Step{Type: "run", Name: "command01"}, Command: "command01", Environment: map[string]string{}}, + }, + }, + }, + }, } for _, tt := range tests { diff --git a/internal/services/runservice/executor/driver/docker.go b/internal/services/runservice/executor/driver/docker.go index bffe2d3..004f9f4 100644 --- a/internal/services/runservice/executor/driver/docker.go +++ b/internal/services/runservice/executor/driver/docker.go @@ -118,7 +118,7 @@ func (d *DockerDriver) NewPod(ctx context.Context, podConfig *PodConfig, out io. // by default always try to pull the image so we are sure only authorized users can fetch them // see https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#alwayspullimages - reader, err := d.client.ImagePull(ctx, containerConfig.Image, types.ImagePullOptions{}) + reader, err := d.client.ImagePull(ctx, containerConfig.Image, types.ImagePullOptions{RegistryAuth: containerConfig.RegistryAuth}) if err != nil { return nil, err } diff --git a/internal/services/runservice/executor/executor.go b/internal/services/runservice/executor/executor.go index 8bbdb03..f572a02 100644 --- a/internal/services/runservice/executor/executor.go +++ b/internal/services/runservice/executor/executor.go @@ -487,17 +487,23 @@ func (e *Executor) setupTask(ctx context.Context, rt *runningTask) error { log.Debugf("starting pod") + registryAuth, err := registryAuthToken(et.Containers[0].Auth) + if err != nil { + return err + } + podConfig := &driver.PodConfig{ Labels: createTaskLabels(et.ID), InitVolumeDir: toolboxContainerDir, Containers: []*driver.ContainerConfig{ { - Image: et.Containers[0].Image, - Cmd: cmd, - Env: et.Containers[0].Environment, - WorkingDir: et.WorkingDir, - User: et.Containers[0].User, - Privileged: et.Containers[0].Privileged, + Image: et.Containers[0].Image, + Cmd: cmd, + Env: et.Containers[0].Environment, + WorkingDir: et.WorkingDir, + User: et.Containers[0].User, + Privileged: et.Containers[0].Privileged, + RegistryAuth: registryAuth, }, }, } @@ -515,7 +521,7 @@ func (e *Executor) setupTask(ctx context.Context, rt *runningTask) error { outf.WriteString("Starting pod.\n") pod, err := e.driver.NewPod(ctx, podConfig, outf) if err != nil { - outf.WriteString("Pod failed to start.\n") + outf.WriteString(fmt.Sprintf("Pod failed to start. Error: %s\n", err)) return err } outf.WriteString("Pod started.\n") diff --git a/internal/services/runservice/executor/registry.go b/internal/services/runservice/executor/registry.go new file mode 100644 index 0000000..45c56ea --- /dev/null +++ b/internal/services/runservice/executor/registry.go @@ -0,0 +1,47 @@ +// Copyright 2019 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package executor + +import ( + "encoding/base64" + "encoding/json" + + "github.com/sorintlab/agola/internal/services/runservice/types" + + dtypes "github.com/docker/docker/api/types" + "github.com/pkg/errors" +) + +func registryAuthToken(auth *types.RegistryAuth) (string, error) { + if auth == nil { + return "", nil + } + + switch auth.Type { + case types.RegistryAuthTypeDefault: + authConfig := dtypes.AuthConfig{ + Username: auth.Username, + Password: auth.Password, + } + authConfigj, err := json.Marshal(authConfig) + if err != nil { + panic(err) + } + return base64.URLEncoding.EncodeToString(authConfigj), nil + + default: + return "", errors.Errorf("unsupported registry auth type %q", auth.Type) + } +} diff --git a/internal/services/runservice/types/types.go b/internal/services/runservice/types/types.go index 705ec04..7cd1815 100644 --- a/internal/services/runservice/types/types.go +++ b/internal/services/runservice/types/types.go @@ -332,6 +332,20 @@ const ( RuntimeTypePod RuntimeType = "pod" ) +type RegistryAuthType string + +const ( + RegistryAuthTypeDefault RegistryAuthType = "default" +) + +type RegistryAuth struct { + Type RegistryAuthType `yaml:"type"` + + // default auth + Username string `yaml:"username"` + Password string `yaml:"password"` +} + type Runtime struct { Type RuntimeType `json:"type,omitempty"` Containers []*Container `json:"containers,omitempty"` @@ -478,6 +492,7 @@ type ExecutorTaskStepStatus struct { type Container struct { Image string `json:"image,omitempty"` + Auth *RegistryAuth `json:"auth,omitempty"` Environment map[string]string `json:"environment,omitempty"` User string `json:"user,omitempty"` Privileged bool `json:"privileged"`