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:
Simone Gotti 2019-04-10 17:13:51 +02:00
parent 298ffc3529
commit 634a8a543c
8 changed files with 329 additions and 22 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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