runservice: implement docker registry auth
By now just support default username/password login In future also support additional container registries with their own credential helpers
This commit is contained in:
parent
298ffc3529
commit
634a8a543c
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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"},
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"`
|
||||
|
|
Loading…
Reference in New Issue