Merge pull request #1617 from simonferquel/pull-secrets

Add support for Kubernetes Pull secrets and Pull policies
This commit is contained in:
Sebastiaan van Stijn 2019-01-31 13:37:45 +01:00 committed by GitHub
commit 5486cddbd9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 183 additions and 18 deletions

View File

@ -19,14 +19,23 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
const (
// pullSecretExtraField is an extra field on ServiceConfigs usable to reference a pull secret
pullSecretExtraField = "x-pull-secret"
// pullPolicyExtraField is an extra field on ServiceConfigs usable to specify a pull policy
pullPolicyExtraField = "x-pull-policy"
)
// NewStackConverter returns a converter from types.Config (compose) to the specified // NewStackConverter returns a converter from types.Config (compose) to the specified
// stack version or error out if the version is not supported or existent. // stack version or error out if the version is not supported or existent.
func NewStackConverter(version string) (StackConverter, error) { func NewStackConverter(version string) (StackConverter, error) {
switch version { switch version {
case "v1beta1": case "v1beta1":
return stackV1Beta1Converter{}, nil return stackV1Beta1Converter{}, nil
case "v1beta2", "v1alpha3": case "v1beta2":
return stackV1Beta2OrHigherConverter{}, nil return stackV1Beta2Converter{}, nil
case "v1alpha3":
return stackV1Alpha3Converter{}, nil
default: default:
return nil, errors.Errorf("stack version %s unsupported", version) return nil, errors.Errorf("stack version %s unsupported", version)
} }
@ -41,7 +50,7 @@ type stackV1Beta1Converter struct{}
func (s stackV1Beta1Converter) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) { func (s stackV1Beta1Converter) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) {
cfg.Version = v1beta1.MaxComposeVersion cfg.Version = v1beta1.MaxComposeVersion
st, err := fromCompose(stderr, name, cfg) st, err := fromCompose(stderr, name, cfg, v1beta1Capabilities)
if err != nil { if err != nil {
return Stack{}, err return Stack{}, err
} }
@ -62,16 +71,26 @@ func (s stackV1Beta1Converter) FromCompose(stderr io.Writer, name string, cfg *c
return st, nil return st, nil
} }
type stackV1Beta2OrHigherConverter struct{} type stackV1Beta2Converter struct{}
func (s stackV1Beta2OrHigherConverter) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) { func (s stackV1Beta2Converter) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) {
return fromCompose(stderr, name, cfg) return fromCompose(stderr, name, cfg, v1beta2Capabilities)
} }
func fromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) { type stackV1Alpha3Converter struct{}
func (s stackV1Alpha3Converter) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) {
return fromCompose(stderr, name, cfg, v1alpha3Capabilities)
}
func fromCompose(stderr io.Writer, name string, cfg *composetypes.Config, capabilities composeCapabilities) (Stack, error) {
spec, err := fromComposeConfig(stderr, cfg, capabilities)
if err != nil {
return Stack{}, err
}
return Stack{ return Stack{
Name: name, Name: name,
Spec: fromComposeConfig(stderr, cfg), Spec: spec,
}, nil }, nil
} }
@ -95,11 +114,15 @@ func stackFromV1beta1(in *v1beta1.Stack) (Stack, error) {
if err != nil { if err != nil {
return Stack{}, err return Stack{}, err
} }
spec, err := fromComposeConfig(ioutil.Discard, cfg, v1beta1Capabilities)
if err != nil {
return Stack{}, err
}
return Stack{ return Stack{
Name: in.ObjectMeta.Name, Name: in.ObjectMeta.Name,
Namespace: in.ObjectMeta.Namespace, Namespace: in.ObjectMeta.Namespace,
ComposeFile: in.Spec.ComposeFile, ComposeFile: in.Spec.ComposeFile,
Spec: fromComposeConfig(ioutil.Discard, cfg), Spec: spec,
}, nil }, nil
} }
@ -162,20 +185,24 @@ func stackToV1alpha3(s Stack) *latest.Stack {
} }
} }
func fromComposeConfig(stderr io.Writer, c *composeTypes.Config) *latest.StackSpec { func fromComposeConfig(stderr io.Writer, c *composeTypes.Config, capabilities composeCapabilities) (*latest.StackSpec, error) {
if c == nil { if c == nil {
return nil return nil, nil
} }
warnUnsupportedFeatures(stderr, c) warnUnsupportedFeatures(stderr, c)
serviceConfigs := make([]latest.ServiceConfig, len(c.Services)) serviceConfigs := make([]latest.ServiceConfig, len(c.Services))
for i, s := range c.Services { for i, s := range c.Services {
serviceConfigs[i] = fromComposeServiceConfig(s) svc, err := fromComposeServiceConfig(s, capabilities)
if err != nil {
return nil, err
}
serviceConfigs[i] = svc
} }
return &latest.StackSpec{ return &latest.StackSpec{
Services: serviceConfigs, Services: serviceConfigs,
Secrets: fromComposeSecrets(c.Secrets), Secrets: fromComposeSecrets(c.Secrets),
Configs: fromComposeConfigs(c.Configs), Configs: fromComposeConfigs(c.Configs),
} }, nil
} }
func fromComposeSecrets(s map[string]composeTypes.SecretConfig) map[string]latest.SecretConfig { func fromComposeSecrets(s map[string]composeTypes.SecretConfig) map[string]latest.SecretConfig {
@ -216,8 +243,13 @@ func fromComposeConfigs(s map[string]composeTypes.ConfigObjConfig) map[string]la
return m return m
} }
func fromComposeServiceConfig(s composeTypes.ServiceConfig) latest.ServiceConfig { func fromComposeServiceConfig(s composeTypes.ServiceConfig, capabilities composeCapabilities) (latest.ServiceConfig, error) {
var userID *int64 var (
userID *int64
pullSecret string
pullPolicy string
err error
)
if s.User != "" { if s.User != "" {
numerical, err := strconv.Atoi(s.User) numerical, err := strconv.Atoi(s.User)
if err == nil { if err == nil {
@ -225,6 +257,20 @@ func fromComposeServiceConfig(s composeTypes.ServiceConfig) latest.ServiceConfig
userID = &unixUserID userID = &unixUserID
} }
} }
pullSecret, err = resolveServiceExtra(s, pullSecretExtraField)
if err != nil {
return latest.ServiceConfig{}, err
}
pullPolicy, err = resolveServiceExtra(s, pullPolicyExtraField)
if err != nil {
return latest.ServiceConfig{}, err
}
if pullSecret != "" && !capabilities.hasPullSecrets {
return latest.ServiceConfig{}, errors.Errorf("stack API version %s does not support pull secrets (field %q), please use version v1alpha3 or higher", capabilities.apiVersion, pullSecretExtraField)
}
if pullPolicy != "" && !capabilities.hasPullPolicies {
return latest.ServiceConfig{}, errors.Errorf("stack API version %s does not support pull policies (field %q), please use version v1alpha3 or higher", capabilities.apiVersion, pullPolicyExtraField)
}
return latest.ServiceConfig{ return latest.ServiceConfig{
Name: s.Name, Name: s.Name,
CapAdd: s.CapAdd, CapAdd: s.CapAdd,
@ -260,7 +306,20 @@ func fromComposeServiceConfig(s composeTypes.ServiceConfig) latest.ServiceConfig
User: userID, User: userID,
Volumes: fromComposeServiceVolumeConfig(s.Volumes), Volumes: fromComposeServiceVolumeConfig(s.Volumes),
WorkingDir: s.WorkingDir, WorkingDir: s.WorkingDir,
PullSecret: pullSecret,
PullPolicy: pullPolicy,
}, nil
}
func resolveServiceExtra(s composeTypes.ServiceConfig, field string) (string, error) {
if iface, ok := s.Extras[field]; ok {
value, ok := iface.(string)
if !ok {
return "", errors.Errorf("field %q: value %v type is %T, should be a string", field, iface, iface)
}
return value, nil
} }
return "", nil
} }
func fromComposePorts(ports []composeTypes.ServicePortConfig) []latest.ServicePortConfig { func fromComposePorts(ports []composeTypes.ServicePortConfig) []latest.ServicePortConfig {
@ -421,3 +480,23 @@ func fromComposeServiceVolumeConfig(vs []composeTypes.ServiceVolumeConfig) []lat
} }
return volumes return volumes
} }
var (
v1beta1Capabilities = composeCapabilities{
apiVersion: "v1beta1",
}
v1beta2Capabilities = composeCapabilities{
apiVersion: "v1beta2",
}
v1alpha3Capabilities = composeCapabilities{
apiVersion: "v1alpha3",
hasPullSecrets: true,
hasPullPolicies: true,
}
)
type composeCapabilities struct {
apiVersion string
hasPullSecrets bool
hasPullPolicies bool
}

View File

@ -1,9 +1,13 @@
package kubernetes package kubernetes
import ( import (
"fmt"
"io/ioutil"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/docker/cli/cli/compose/loader"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/compose-on-kubernetes/api/compose/v1alpha3" "github.com/docker/compose-on-kubernetes/api/compose/v1alpha3"
"github.com/docker/compose-on-kubernetes/api/compose/v1beta1" "github.com/docker/compose-on-kubernetes/api/compose/v1beta1"
"github.com/docker/compose-on-kubernetes/api/compose/v1beta2" "github.com/docker/compose-on-kubernetes/api/compose/v1beta2"
@ -161,3 +165,73 @@ func TestConvertFromToV1alpha3(t *testing.T) {
gotBack := stackToV1alpha3(result) gotBack := stackToV1alpha3(result)
assert.DeepEqual(t, stackv1alpha3, gotBack) assert.DeepEqual(t, stackv1alpha3, gotBack)
} }
func loadTestStackWith(t *testing.T, with string) *composetypes.Config {
t.Helper()
filePath := fmt.Sprintf("testdata/compose-with-%s.yml", with)
data, err := ioutil.ReadFile(filePath)
assert.NilError(t, err)
yamlData, err := loader.ParseYAML(data)
assert.NilError(t, err)
cfg, err := loader.Load(composetypes.ConfigDetails{
ConfigFiles: []composetypes.ConfigFile{
{Config: yamlData, Filename: filePath},
},
})
assert.NilError(t, err)
return cfg
}
func TestHandlePullSecret(t *testing.T) {
testData := loadTestStackWith(t, "pull-secret")
cases := []struct {
version string
err string
}{
{version: "v1beta1", err: `stack API version v1beta1 does not support pull secrets (field "x-pull-secret"), please use version v1alpha3 or higher`},
{version: "v1beta2", err: `stack API version v1beta2 does not support pull secrets (field "x-pull-secret"), please use version v1alpha3 or higher`},
{version: "v1alpha3"},
}
for _, c := range cases {
t.Run(c.version, func(t *testing.T) {
conv, err := NewStackConverter(c.version)
assert.NilError(t, err)
s, err := conv.FromCompose(ioutil.Discard, "test", testData)
if c.err != "" {
assert.Error(t, err, c.err)
} else {
assert.NilError(t, err)
assert.Equal(t, s.Spec.Services[0].PullSecret, "some-secret")
}
})
}
}
func TestHandlePullPolicy(t *testing.T) {
testData := loadTestStackWith(t, "pull-policy")
cases := []struct {
version string
err string
}{
{version: "v1beta1", err: `stack API version v1beta1 does not support pull policies (field "x-pull-policy"), please use version v1alpha3 or higher`},
{version: "v1beta2", err: `stack API version v1beta2 does not support pull policies (field "x-pull-policy"), please use version v1alpha3 or higher`},
{version: "v1alpha3"},
}
for _, c := range cases {
t.Run(c.version, func(t *testing.T) {
conv, err := NewStackConverter(c.version)
assert.NilError(t, err)
s, err := conv.FromCompose(ioutil.Discard, "test", testData)
if c.err != "" {
assert.Error(t, err, c.err)
} else {
assert.NilError(t, err)
assert.Equal(t, s.Spec.Services[0].PullPolicy, "Never")
}
})
}
}

View File

@ -125,7 +125,7 @@ func verify(services corev1.ServiceInterface, stackName string, service string)
// stackV1Beta2 implements stackClient interface and talks to compose component v1beta2. // stackV1Beta2 implements stackClient interface and talks to compose component v1beta2.
type stackV1Beta2 struct { type stackV1Beta2 struct {
stackV1Beta2OrHigherConverter stackV1Beta2Converter
stacks composev1beta2.StackInterface stacks composev1beta2.StackInterface
} }
@ -203,7 +203,7 @@ func (s *stackV1Beta2) IsColliding(servicesClient corev1.ServiceInterface, st St
// stackV1Beta2 implements stackClient interface and talks to compose component v1beta2. // stackV1Beta2 implements stackClient interface and talks to compose component v1beta2.
type stackV1Alpha3 struct { type stackV1Alpha3 struct {
stackV1Beta2OrHigherConverter stackV1Alpha3Converter
stacks composev1alpha3.StackInterface stacks composev1alpha3.StackInterface
} }

View File

@ -0,0 +1,5 @@
version: "3.7"
services:
test:
image: "some-image"
x-pull-policy: "Never"

View File

@ -0,0 +1,5 @@
version: "3.7"
services:
test:
image: "some-private-image"
x-pull-secret: "some-secret"

View File

@ -14,7 +14,7 @@ github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76 # v1.1.0
github.com/dgrijalva/jwt-go a2c85815a77d0f951e33ba4db5ae93629a1530af github.com/dgrijalva/jwt-go a2c85815a77d0f951e33ba4db5ae93629a1530af
github.com/docker/distribution 83389a148052d74ac602f5f1d62f86ff2f3c4aa5 github.com/docker/distribution 83389a148052d74ac602f5f1d62f86ff2f3c4aa5
github.com/docker/docker f76d6a078d881f410c00e8d900dcdfc2e026c841 github.com/docker/docker f76d6a078d881f410c00e8d900dcdfc2e026c841
github.com/docker/compose-on-kubernetes 1559927c6b456d56cc9c9b05438252ebb646640b # master w/ v1alpha3 github.com/docker/compose-on-kubernetes 356b2919c496f7e988f6e0dfe7e67d919602e14e # master w/ v1alpha3+pullsecrets+pull-policy
github.com/docker/docker-credential-helpers 5241b46610f2491efdf9d1c85f1ddf5b02f6d962 github.com/docker/docker-credential-helpers 5241b46610f2491efdf9d1c85f1ddf5b02f6d962
# the docker/go package contains a customized version of canonical/json # the docker/go package contains a customized version of canonical/json
# and is used by Notary. The package is periodically rebased on current Go versions. # and is used by Notary. The package is periodically rebased on current Go versions.

View File

@ -97,6 +97,8 @@ type ServiceConfig struct {
User *int64 `json:"user,omitempty"` User *int64 `json:"user,omitempty"`
Volumes []ServiceVolumeConfig `json:"volumes,omitempty"` Volumes []ServiceVolumeConfig `json:"volumes,omitempty"`
WorkingDir string `json:"working_dir,omitempty"` WorkingDir string `json:"working_dir,omitempty"`
PullSecret string `json:"pull_secret,omitempty"`
PullPolicy string `json:"pull_policy,omitempty"`
} }
// ServicePortConfig is the port configuration for a service // ServicePortConfig is the port configuration for a service