diff --git a/cli/command/stack/kubernetes/convert.go b/cli/command/stack/kubernetes/convert.go index 21500d7c5838..a9cf5ba35149 100644 --- a/cli/command/stack/kubernetes/convert.go +++ b/cli/command/stack/kubernetes/convert.go @@ -19,14 +19,23 @@ import ( 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 // stack version or error out if the version is not supported or existent. func NewStackConverter(version string) (StackConverter, error) { switch version { case "v1beta1": return stackV1Beta1Converter{}, nil - case "v1beta2", "v1alpha3": - return stackV1Beta2OrHigherConverter{}, nil + case "v1beta2": + return stackV1Beta2Converter{}, nil + case "v1alpha3": + return stackV1Alpha3Converter{}, nil default: 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) { cfg.Version = v1beta1.MaxComposeVersion - st, err := fromCompose(stderr, name, cfg) + st, err := fromCompose(stderr, name, cfg, v1beta1Capabilities) if err != nil { return Stack{}, err } @@ -62,16 +71,26 @@ func (s stackV1Beta1Converter) FromCompose(stderr io.Writer, name string, cfg *c return st, nil } -type stackV1Beta2OrHigherConverter struct{} +type stackV1Beta2Converter struct{} -func (s stackV1Beta2OrHigherConverter) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) { - return fromCompose(stderr, name, cfg) +func (s stackV1Beta2Converter) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) { + 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{ Name: name, - Spec: fromComposeConfig(stderr, cfg), + Spec: spec, }, nil } @@ -95,11 +114,15 @@ func stackFromV1beta1(in *v1beta1.Stack) (Stack, error) { if err != nil { return Stack{}, err } + spec, err := fromComposeConfig(ioutil.Discard, cfg, v1beta1Capabilities) + if err != nil { + return Stack{}, err + } return Stack{ Name: in.ObjectMeta.Name, Namespace: in.ObjectMeta.Namespace, ComposeFile: in.Spec.ComposeFile, - Spec: fromComposeConfig(ioutil.Discard, cfg), + Spec: spec, }, 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 { - return nil + return nil, nil } warnUnsupportedFeatures(stderr, c) serviceConfigs := make([]latest.ServiceConfig, len(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{ Services: serviceConfigs, Secrets: fromComposeSecrets(c.Secrets), Configs: fromComposeConfigs(c.Configs), - } + }, nil } 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 } -func fromComposeServiceConfig(s composeTypes.ServiceConfig) latest.ServiceConfig { - var userID *int64 +func fromComposeServiceConfig(s composeTypes.ServiceConfig, capabilities composeCapabilities) (latest.ServiceConfig, error) { + var ( + userID *int64 + pullSecret string + pullPolicy string + err error + ) if s.User != "" { numerical, err := strconv.Atoi(s.User) if err == nil { @@ -225,6 +257,20 @@ func fromComposeServiceConfig(s composeTypes.ServiceConfig) latest.ServiceConfig 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{ Name: s.Name, CapAdd: s.CapAdd, @@ -260,7 +306,20 @@ func fromComposeServiceConfig(s composeTypes.ServiceConfig) latest.ServiceConfig User: userID, Volumes: fromComposeServiceVolumeConfig(s.Volumes), 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 { @@ -421,3 +480,23 @@ func fromComposeServiceVolumeConfig(vs []composeTypes.ServiceVolumeConfig) []lat } 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 +} diff --git a/cli/command/stack/kubernetes/convert_test.go b/cli/command/stack/kubernetes/convert_test.go index 3f7f9e967c47..e8ce63a67c66 100644 --- a/cli/command/stack/kubernetes/convert_test.go +++ b/cli/command/stack/kubernetes/convert_test.go @@ -1,9 +1,13 @@ package kubernetes import ( + "fmt" + "io/ioutil" "path/filepath" "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/v1beta1" "github.com/docker/compose-on-kubernetes/api/compose/v1beta2" @@ -161,3 +165,73 @@ func TestConvertFromToV1alpha3(t *testing.T) { gotBack := stackToV1alpha3(result) 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") + } + }) + } +} diff --git a/cli/command/stack/kubernetes/stackclient.go b/cli/command/stack/kubernetes/stackclient.go index 53670e4d80c4..5ce4480ebb00 100644 --- a/cli/command/stack/kubernetes/stackclient.go +++ b/cli/command/stack/kubernetes/stackclient.go @@ -125,7 +125,7 @@ func verify(services corev1.ServiceInterface, stackName string, service string) // stackV1Beta2 implements stackClient interface and talks to compose component v1beta2. type stackV1Beta2 struct { - stackV1Beta2OrHigherConverter + stackV1Beta2Converter 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. type stackV1Alpha3 struct { - stackV1Beta2OrHigherConverter + stackV1Alpha3Converter stacks composev1alpha3.StackInterface } diff --git a/cli/command/stack/kubernetes/testdata/compose-with-pull-policy.yml b/cli/command/stack/kubernetes/testdata/compose-with-pull-policy.yml new file mode 100644 index 000000000000..328416ee1c9d --- /dev/null +++ b/cli/command/stack/kubernetes/testdata/compose-with-pull-policy.yml @@ -0,0 +1,5 @@ +version: "3.7" +services: + test: + image: "some-image" + x-pull-policy: "Never" \ No newline at end of file diff --git a/cli/command/stack/kubernetes/testdata/compose-with-pull-secret.yml b/cli/command/stack/kubernetes/testdata/compose-with-pull-secret.yml new file mode 100644 index 000000000000..8510f5c541b0 --- /dev/null +++ b/cli/command/stack/kubernetes/testdata/compose-with-pull-secret.yml @@ -0,0 +1,5 @@ +version: "3.7" +services: + test: + image: "some-private-image" + x-pull-secret: "some-secret" \ No newline at end of file