From 2e5981d613310dc2ff5b789ff13d198dd8a95169 Mon Sep 17 00:00:00 2001 From: Simon Ferquel Date: Wed, 16 Jan 2019 12:56:37 +0100 Subject: [PATCH] Handle version v1alpha3 Signed-off-by: Simon Ferquel --- cli/command/stack/kubernetes/cli.go | 2 +- cli/command/stack/kubernetes/client.go | 12 +- cli/command/stack/kubernetes/convert.go | 130 ++++++++++++------- cli/command/stack/kubernetes/convert_test.go | 2 + cli/command/stack/kubernetes/deploy_test.go | 90 ++++++++++++- cli/command/stack/kubernetes/stack.go | 4 +- cli/command/stack/kubernetes/stackclient.go | 92 ++++++++++++- cli/command/system/version.go | 8 +- kubernetes/check.go | 60 +++++++-- kubernetes/check_test.go | 54 ++++++++ 10 files changed, 375 insertions(+), 79 deletions(-) create mode 100644 kubernetes/check_test.go diff --git a/cli/command/stack/kubernetes/cli.go b/cli/command/stack/kubernetes/cli.go index a531846809..8f5baf6579 100644 --- a/cli/command/stack/kubernetes/cli.go +++ b/cli/command/stack/kubernetes/cli.go @@ -103,7 +103,7 @@ func WrapCli(dockerCli command.Cli, opts Options) (*KubeCli, error) { } func (c *KubeCli) composeClient() (*Factory, error) { - return NewFactory(c.kubeNamespace, c.kubeConfig, c.clientSet) + return NewFactory(c.kubeNamespace, c.kubeConfig, c.clientSet, c.ClientInfo().HasExperimental) } func (c *KubeCli) checkHostsMatch() error { diff --git a/cli/command/stack/kubernetes/client.go b/cli/command/stack/kubernetes/client.go index 28bb484d3a..fd516bb5d2 100644 --- a/cli/command/stack/kubernetes/client.go +++ b/cli/command/stack/kubernetes/client.go @@ -1,7 +1,7 @@ package kubernetes import ( - kubernetes "github.com/docker/compose-on-kubernetes/api" + "github.com/docker/cli/kubernetes" "github.com/pkg/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kubeclient "k8s.io/client-go/kubernetes" @@ -18,10 +18,11 @@ type Factory struct { coreClientSet corev1.CoreV1Interface appsClientSet appsv1beta2.AppsV1beta2Interface clientSet *kubeclient.Clientset + experimental bool } // NewFactory creates a kubernetes client factory -func NewFactory(namespace string, config *restclient.Config, clientSet *kubeclient.Clientset) (*Factory, error) { +func NewFactory(namespace string, config *restclient.Config, clientSet *kubeclient.Clientset, experimental bool) (*Factory, error) { coreClientSet, err := corev1.NewForConfig(config) if err != nil { return nil, err @@ -38,6 +39,7 @@ func NewFactory(namespace string, config *restclient.Config, clientSet *kubeclie coreClientSet: coreClientSet, appsClientSet: appsClientSet, clientSet: clientSet, + experimental: experimental, }, nil } @@ -83,7 +85,7 @@ func (s *Factory) DaemonSets() typesappsv1beta2.DaemonSetInterface { // Stacks returns a client for Docker's Stack on Kubernetes func (s *Factory) Stacks(allNamespaces bool) (StackClient, error) { - version, err := kubernetes.GetStackAPIVersion(s.clientSet) + version, err := kubernetes.GetStackAPIVersion(s.clientSet.Discovery(), s.experimental) if err != nil { return nil, err } @@ -97,7 +99,9 @@ func (s *Factory) Stacks(allNamespaces bool) (StackClient, error) { return newStackV1Beta1(s.config, namespace) case kubernetes.StackAPIV1Beta2: return newStackV1Beta2(s.config, namespace) + case kubernetes.StackAPIV1Alpha3: + return newStackV1Alpha3(s.config, namespace) default: - return nil, errors.Errorf("no supported Stack API version") + return nil, errors.Errorf("unsupported stack API version: %q", version) } } diff --git a/cli/command/stack/kubernetes/convert.go b/cli/command/stack/kubernetes/convert.go index c76be93cad..21500d7c58 100644 --- a/cli/command/stack/kubernetes/convert.go +++ b/cli/command/stack/kubernetes/convert.go @@ -11,6 +11,7 @@ import ( "github.com/docker/cli/cli/compose/schema" composeTypes "github.com/docker/cli/cli/compose/types" composetypes "github.com/docker/cli/cli/compose/types" + latest "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" "github.com/pkg/errors" @@ -24,8 +25,8 @@ func NewStackConverter(version string) (StackConverter, error) { switch version { case "v1beta1": return stackV1Beta1Converter{}, nil - case "v1beta2": - return stackV1Beta2Converter{}, nil + case "v1beta2", "v1alpha3": + return stackV1Beta2OrHigherConverter{}, nil default: return nil, errors.Errorf("stack version %s unsupported", version) } @@ -61,9 +62,9 @@ func (s stackV1Beta1Converter) FromCompose(stderr io.Writer, name string, cfg *c return st, nil } -type stackV1Beta2Converter struct{} +type stackV1Beta2OrHigherConverter struct{} -func (s stackV1Beta2Converter) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) { +func (s stackV1Beta2OrHigherConverter) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) { return fromCompose(stderr, name, cfg) } @@ -113,7 +114,38 @@ func stackToV1beta1(s Stack) *v1beta1.Stack { } } -func stackFromV1beta2(in *v1beta2.Stack) Stack { +func stackFromV1beta2(in *v1beta2.Stack) (Stack, error) { + var spec *latest.StackSpec + if in.Spec != nil { + spec = &latest.StackSpec{} + if err := latest.Convert_v1beta2_StackSpec_To_v1alpha3_StackSpec(in.Spec, spec, nil); err != nil { + return Stack{}, err + } + } + return Stack{ + Name: in.ObjectMeta.Name, + Namespace: in.ObjectMeta.Namespace, + Spec: spec, + }, nil +} + +func stackToV1beta2(s Stack) (*v1beta2.Stack, error) { + var spec *v1beta2.StackSpec + if s.Spec != nil { + spec = &v1beta2.StackSpec{} + if err := latest.Convert_v1alpha3_StackSpec_To_v1beta2_StackSpec(s.Spec, spec, nil); err != nil { + return nil, err + } + } + return &v1beta2.Stack{ + ObjectMeta: metav1.ObjectMeta{ + Name: s.Name, + }, + Spec: spec, + }, nil +} + +func stackFromV1alpha3(in *latest.Stack) Stack { return Stack{ Name: in.ObjectMeta.Name, Namespace: in.ObjectMeta.Namespace, @@ -121,8 +153,8 @@ func stackFromV1beta2(in *v1beta2.Stack) Stack { } } -func stackToV1beta2(s Stack) *v1beta2.Stack { - return &v1beta2.Stack{ +func stackToV1alpha3(s Stack) *latest.Stack { + return &latest.Stack{ ObjectMeta: metav1.ObjectMeta{ Name: s.Name, }, @@ -130,32 +162,32 @@ func stackToV1beta2(s Stack) *v1beta2.Stack { } } -func fromComposeConfig(stderr io.Writer, c *composeTypes.Config) *v1beta2.StackSpec { +func fromComposeConfig(stderr io.Writer, c *composeTypes.Config) *latest.StackSpec { if c == nil { return nil } warnUnsupportedFeatures(stderr, c) - serviceConfigs := make([]v1beta2.ServiceConfig, len(c.Services)) + serviceConfigs := make([]latest.ServiceConfig, len(c.Services)) for i, s := range c.Services { serviceConfigs[i] = fromComposeServiceConfig(s) } - return &v1beta2.StackSpec{ + return &latest.StackSpec{ Services: serviceConfigs, Secrets: fromComposeSecrets(c.Secrets), Configs: fromComposeConfigs(c.Configs), } } -func fromComposeSecrets(s map[string]composeTypes.SecretConfig) map[string]v1beta2.SecretConfig { +func fromComposeSecrets(s map[string]composeTypes.SecretConfig) map[string]latest.SecretConfig { if s == nil { return nil } - m := map[string]v1beta2.SecretConfig{} + m := map[string]latest.SecretConfig{} for key, value := range s { - m[key] = v1beta2.SecretConfig{ + m[key] = latest.SecretConfig{ Name: value.Name, File: value.File, - External: v1beta2.External{ + External: latest.External{ Name: value.External.Name, External: value.External.External, }, @@ -165,16 +197,16 @@ func fromComposeSecrets(s map[string]composeTypes.SecretConfig) map[string]v1bet return m } -func fromComposeConfigs(s map[string]composeTypes.ConfigObjConfig) map[string]v1beta2.ConfigObjConfig { +func fromComposeConfigs(s map[string]composeTypes.ConfigObjConfig) map[string]latest.ConfigObjConfig { if s == nil { return nil } - m := map[string]v1beta2.ConfigObjConfig{} + m := map[string]latest.ConfigObjConfig{} for key, value := range s { - m[key] = v1beta2.ConfigObjConfig{ + m[key] = latest.ConfigObjConfig{ Name: value.Name, File: value.File, - External: v1beta2.External{ + External: latest.External{ Name: value.External.Name, External: value.External.External, }, @@ -184,7 +216,7 @@ func fromComposeConfigs(s map[string]composeTypes.ConfigObjConfig) map[string]v1 return m } -func fromComposeServiceConfig(s composeTypes.ServiceConfig) v1beta2.ServiceConfig { +func fromComposeServiceConfig(s composeTypes.ServiceConfig) latest.ServiceConfig { var userID *int64 if s.User != "" { numerical, err := strconv.Atoi(s.User) @@ -193,13 +225,13 @@ func fromComposeServiceConfig(s composeTypes.ServiceConfig) v1beta2.ServiceConfi userID = &unixUserID } } - return v1beta2.ServiceConfig{ + return latest.ServiceConfig{ Name: s.Name, CapAdd: s.CapAdd, CapDrop: s.CapDrop, Command: s.Command, Configs: fromComposeServiceConfigs(s.Configs), - Deploy: v1beta2.DeployConfig{ + Deploy: latest.DeployConfig{ Mode: s.Deploy.Mode, Replicas: s.Deploy.Replicas, Labels: s.Deploy.Labels, @@ -231,13 +263,13 @@ func fromComposeServiceConfig(s composeTypes.ServiceConfig) v1beta2.ServiceConfi } } -func fromComposePorts(ports []composeTypes.ServicePortConfig) []v1beta2.ServicePortConfig { +func fromComposePorts(ports []composeTypes.ServicePortConfig) []latest.ServicePortConfig { if ports == nil { return nil } - p := make([]v1beta2.ServicePortConfig, len(ports)) + p := make([]latest.ServicePortConfig, len(ports)) for i, port := range ports { - p[i] = v1beta2.ServicePortConfig{ + p[i] = latest.ServicePortConfig{ Mode: port.Mode, Target: port.Target, Published: port.Published, @@ -247,13 +279,13 @@ func fromComposePorts(ports []composeTypes.ServicePortConfig) []v1beta2.ServiceP return p } -func fromComposeServiceSecrets(secrets []composeTypes.ServiceSecretConfig) []v1beta2.ServiceSecretConfig { +func fromComposeServiceSecrets(secrets []composeTypes.ServiceSecretConfig) []latest.ServiceSecretConfig { if secrets == nil { return nil } - c := make([]v1beta2.ServiceSecretConfig, len(secrets)) + c := make([]latest.ServiceSecretConfig, len(secrets)) for i, secret := range secrets { - c[i] = v1beta2.ServiceSecretConfig{ + c[i] = latest.ServiceSecretConfig{ Source: secret.Source, Target: secret.Target, UID: secret.UID, @@ -263,13 +295,13 @@ func fromComposeServiceSecrets(secrets []composeTypes.ServiceSecretConfig) []v1b return c } -func fromComposeServiceConfigs(configs []composeTypes.ServiceConfigObjConfig) []v1beta2.ServiceConfigObjConfig { +func fromComposeServiceConfigs(configs []composeTypes.ServiceConfigObjConfig) []latest.ServiceConfigObjConfig { if configs == nil { return nil } - c := make([]v1beta2.ServiceConfigObjConfig, len(configs)) + c := make([]latest.ServiceConfigObjConfig, len(configs)) for i, config := range configs { - c[i] = v1beta2.ServiceConfigObjConfig{ + c[i] = latest.ServiceConfigObjConfig{ Source: config.Source, Target: config.Target, UID: config.UID, @@ -279,11 +311,11 @@ func fromComposeServiceConfigs(configs []composeTypes.ServiceConfigObjConfig) [] return c } -func fromComposeHealthcheck(h *composeTypes.HealthCheckConfig) *v1beta2.HealthCheckConfig { +func fromComposeHealthcheck(h *composeTypes.HealthCheckConfig) *latest.HealthCheckConfig { if h == nil { return nil } - return &v1beta2.HealthCheckConfig{ + return &latest.HealthCheckConfig{ Test: h.Test, Timeout: composetypes.ConvertDurationPtr(h.Timeout), Interval: composetypes.ConvertDurationPtr(h.Interval), @@ -291,8 +323,8 @@ func fromComposeHealthcheck(h *composeTypes.HealthCheckConfig) *v1beta2.HealthCh } } -func fromComposePlacement(p composeTypes.Placement) v1beta2.Placement { - return v1beta2.Placement{ +func fromComposePlacement(p composeTypes.Placement) latest.Placement { + return latest.Placement{ Constraints: fromComposeConstraints(p.Constraints), } } @@ -306,18 +338,18 @@ const ( swarmLabelPrefix = "node.labels." ) -func fromComposeConstraints(s []string) *v1beta2.Constraints { +func fromComposeConstraints(s []string) *latest.Constraints { if len(s) == 0 { return nil } - constraints := &v1beta2.Constraints{} + constraints := &latest.Constraints{} for _, constraint := range s { matches := constraintEquals.FindStringSubmatch(constraint) if len(matches) == 4 { key := matches[1] operator := matches[2] value := matches[3] - constraint := &v1beta2.Constraint{ + constraint := &latest.Constraint{ Operator: operator, Value: value, } @@ -330,7 +362,7 @@ func fromComposeConstraints(s []string) *v1beta2.Constraints { constraints.Hostname = constraint case strings.HasPrefix(key, swarmLabelPrefix): if constraints.MatchLabels == nil { - constraints.MatchLabels = map[string]v1beta2.Constraint{} + constraints.MatchLabels = map[string]latest.Constraint{} } constraints.MatchLabels[strings.TrimPrefix(key, swarmLabelPrefix)] = *constraint } @@ -339,48 +371,48 @@ func fromComposeConstraints(s []string) *v1beta2.Constraints { return constraints } -func fromComposeResources(r composeTypes.Resources) v1beta2.Resources { - return v1beta2.Resources{ +func fromComposeResources(r composeTypes.Resources) latest.Resources { + return latest.Resources{ Limits: fromComposeResourcesResource(r.Limits), Reservations: fromComposeResourcesResource(r.Reservations), } } -func fromComposeResourcesResource(r *composeTypes.Resource) *v1beta2.Resource { +func fromComposeResourcesResource(r *composeTypes.Resource) *latest.Resource { if r == nil { return nil } - return &v1beta2.Resource{ + return &latest.Resource{ MemoryBytes: int64(r.MemoryBytes), NanoCPUs: r.NanoCPUs, } } -func fromComposeUpdateConfig(u *composeTypes.UpdateConfig) *v1beta2.UpdateConfig { +func fromComposeUpdateConfig(u *composeTypes.UpdateConfig) *latest.UpdateConfig { if u == nil { return nil } - return &v1beta2.UpdateConfig{ + return &latest.UpdateConfig{ Parallelism: u.Parallelism, } } -func fromComposeRestartPolicy(r *composeTypes.RestartPolicy) *v1beta2.RestartPolicy { +func fromComposeRestartPolicy(r *composeTypes.RestartPolicy) *latest.RestartPolicy { if r == nil { return nil } - return &v1beta2.RestartPolicy{ + return &latest.RestartPolicy{ Condition: r.Condition, } } -func fromComposeServiceVolumeConfig(vs []composeTypes.ServiceVolumeConfig) []v1beta2.ServiceVolumeConfig { +func fromComposeServiceVolumeConfig(vs []composeTypes.ServiceVolumeConfig) []latest.ServiceVolumeConfig { if vs == nil { return nil } - volumes := []v1beta2.ServiceVolumeConfig{} + volumes := []latest.ServiceVolumeConfig{} for _, v := range vs { - volumes = append(volumes, v1beta2.ServiceVolumeConfig{ + volumes = append(volumes, latest.ServiceVolumeConfig{ Type: v.Type, Source: v.Source, Target: v.Target, diff --git a/cli/command/stack/kubernetes/convert_test.go b/cli/command/stack/kubernetes/convert_test.go index 5e0f8dc54d..1c4b2d3e33 100644 --- a/cli/command/stack/kubernetes/convert_test.go +++ b/cli/command/stack/kubernetes/convert_test.go @@ -15,4 +15,6 @@ func TestNewStackConverter(t *testing.T) { assert.NilError(t, err) _, err = NewStackConverter("v1beta2") assert.NilError(t, err) + _, err = NewStackConverter("v1alpha3") + assert.NilError(t, err) } diff --git a/cli/command/stack/kubernetes/deploy_test.go b/cli/command/stack/kubernetes/deploy_test.go index 52e607c050..85d1a5ff7d 100644 --- a/cli/command/stack/kubernetes/deploy_test.go +++ b/cli/command/stack/kubernetes/deploy_test.go @@ -4,8 +4,10 @@ import ( "errors" "testing" + composev1alpha3 "github.com/docker/compose-on-kubernetes/api/client/clientset/typed/compose/v1alpha3" composev1beta1 "github.com/docker/compose-on-kubernetes/api/client/clientset/typed/compose/v1beta1" composev1beta2 "github.com/docker/compose-on-kubernetes/api/client/clientset/typed/compose/v1beta2" + "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" "gotest.tools/assert" @@ -31,11 +33,11 @@ configs: test: file: testdata/config `, - Spec: &v1beta2.StackSpec{ - Configs: map[string]v1beta2.ConfigObjConfig{ + Spec: &v1alpha3.StackSpec{ + Configs: map[string]v1alpha3.ConfigObjConfig{ "test": {Name: "test", File: "testdata/config"}, }, - Secrets: map[string]v1beta2.SecretConfig{ + Secrets: map[string]v1alpha3.SecretConfig{ "test": {Name: "test", File: "testdata/secret"}, }, }, @@ -86,6 +88,24 @@ func TestCreateChildResourcesV1Beta2(t *testing.T) { checkOwnerReferences(t, s.ObjectMeta, "test", v1beta2.SchemeGroupVersion.String()) } +func TestCreateChildResourcesV1Alpha3(t *testing.T) { + k8sclientSet := fake.NewSimpleClientset() + stack := testStack() + configs := k8sclientSet.CoreV1().ConfigMaps("test") + secrets := k8sclientSet.CoreV1().Secrets("test") + assert.NilError(t, createResources( + stack, + &stackV1Alpha3{stacks: &fakeV1alpha3Client{}}, + configs, + secrets)) + c, err := configs.Get("test", metav1.GetOptions{}) + assert.NilError(t, err) + checkOwnerReferences(t, c.ObjectMeta, "test", v1alpha3.SchemeGroupVersion.String()) + s, err := secrets.Get("test", metav1.GetOptions{}) + assert.NilError(t, err) + checkOwnerReferences(t, s.ObjectMeta, "test", v1alpha3.SchemeGroupVersion.String()) +} + func TestCreateChildResourcesWithStackCreationErrorV1Beta1(t *testing.T) { k8sclientSet := fake.NewSimpleClientset() stack := testStack() @@ -120,6 +140,23 @@ func TestCreateChildResourcesWithStackCreationErrorV1Beta2(t *testing.T) { assert.Check(t, kerrors.IsNotFound(err)) } +func TestCreateChildResourcesWithStackCreationErrorV1Alpha3(t *testing.T) { + k8sclientSet := fake.NewSimpleClientset() + stack := testStack() + configs := k8sclientSet.CoreV1().ConfigMaps("test") + secrets := k8sclientSet.CoreV1().Secrets("test") + err := createResources( + stack, + &stackV1Alpha3{stacks: &fakeV1alpha3Client{errorOnCreate: true}}, + configs, + secrets) + assert.Error(t, err, "some error") + _, err = configs.Get("test", metav1.GetOptions{}) + assert.Check(t, kerrors.IsNotFound(err)) + _, err = secrets.Get("test", metav1.GetOptions{}) + assert.Check(t, kerrors.IsNotFound(err)) +} + type fakeV1beta1Client struct { errorOnCreate bool } @@ -213,3 +250,50 @@ func (c *fakeV1beta2Client) Patch(name string, pt types.PatchType, data []byte, func (c *fakeV1beta2Client) WithSkipValidation() composev1beta2.StackInterface { return c } + +type fakeV1alpha3Client struct { + errorOnCreate bool +} + +func (c *fakeV1alpha3Client) Create(s *v1alpha3.Stack) (*v1alpha3.Stack, error) { + if c.errorOnCreate { + return nil, errors.New("some error") + } + return s, nil +} + +func (c *fakeV1alpha3Client) Update(*v1alpha3.Stack) (*v1alpha3.Stack, error) { + return nil, nil +} + +func (c *fakeV1alpha3Client) UpdateStatus(*v1alpha3.Stack) (*v1alpha3.Stack, error) { + return nil, nil +} + +func (c *fakeV1alpha3Client) Delete(name string, options *metav1.DeleteOptions) error { + return nil +} + +func (c *fakeV1alpha3Client) DeleteCollection(options *metav1.DeleteOptions, listOptions metav1.ListOptions) error { + return nil +} + +func (c *fakeV1alpha3Client) Get(name string, options metav1.GetOptions) (*v1alpha3.Stack, error) { + return nil, kerrors.NewNotFound(v1beta1.SchemeGroupVersion.WithResource("stacks").GroupResource(), name) +} + +func (c *fakeV1alpha3Client) List(opts metav1.ListOptions) (*v1alpha3.StackList, error) { + return nil, nil +} + +func (c *fakeV1alpha3Client) Watch(opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} + +func (c *fakeV1alpha3Client) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (*v1alpha3.Stack, error) { + return nil, nil +} + +func (c *fakeV1alpha3Client) WithSkipValidation() composev1alpha3.StackInterface { + return c +} diff --git a/cli/command/stack/kubernetes/stack.go b/cli/command/stack/kubernetes/stack.go index 2a0ee16a5b..e368d718de 100644 --- a/cli/command/stack/kubernetes/stack.go +++ b/cli/command/stack/kubernetes/stack.go @@ -5,7 +5,7 @@ import ( "path/filepath" "sort" - "github.com/docker/compose-on-kubernetes/api/compose/v1beta2" + latest "github.com/docker/compose-on-kubernetes/api/compose/v1alpha3" "github.com/docker/compose-on-kubernetes/api/labels" apiv1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -17,7 +17,7 @@ type Stack struct { Name string Namespace string ComposeFile string - Spec *v1beta2.StackSpec + Spec *latest.StackSpec } type childResource interface { diff --git a/cli/command/stack/kubernetes/stackclient.go b/cli/command/stack/kubernetes/stackclient.go index 513b438957..53670e4d80 100644 --- a/cli/command/stack/kubernetes/stackclient.go +++ b/cli/command/stack/kubernetes/stackclient.go @@ -3,8 +3,10 @@ package kubernetes import ( "fmt" + composev1alpha3 "github.com/docker/compose-on-kubernetes/api/client/clientset/typed/compose/v1alpha3" composev1beta1 "github.com/docker/compose-on-kubernetes/api/client/clientset/typed/compose/v1beta1" composev1beta2 "github.com/docker/compose-on-kubernetes/api/client/clientset/typed/compose/v1beta2" + "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" "github.com/docker/compose-on-kubernetes/api/labels" @@ -123,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 { - stackV1Beta2Converter + stackV1Beta2OrHigherConverter stacks composev1beta2.StackInterface } @@ -136,17 +138,21 @@ func newStackV1Beta2(config *rest.Config, namespace string) (*stackV1Beta2, erro } func (s *stackV1Beta2) CreateOrUpdate(internalStack Stack, childResources []childResource) error { - // If it already exists, update the stack var ( stack *v1beta2.Stack err error ) + resolved, err := stackToV1beta2(internalStack) + if err != nil { + deleteChildResources(childResources) + return err + } if stack, err = s.stacks.Get(internalStack.Name, metav1.GetOptions{}); err == nil { - stack.Spec = internalStack.Spec + stack.Spec = resolved.Spec stack, err = s.stacks.Update(stack) } else { // Or create it - stack, err = s.stacks.Create(stackToV1beta2(internalStack)) + stack, err = s.stacks.Create(resolved) } if err != nil { deleteChildResources(childResources) @@ -173,7 +179,7 @@ func (s *stackV1Beta2) Get(name string) (Stack, error) { if err != nil { return Stack{}, err } - return stackFromV1beta2(stackBeta2), nil + return stackFromV1beta2(stackBeta2) } func (s *stackV1Beta2) List(opts metav1.ListOptions) ([]Stack, error) { @@ -183,7 +189,9 @@ func (s *stackV1Beta2) List(opts metav1.ListOptions) ([]Stack, error) { } stacks := make([]Stack, len(list.Items)) for i := range list.Items { - stacks[i] = stackFromV1beta2(&list.Items[i]) + if stacks[i], err = stackFromV1beta2(&list.Items[i]); err != nil { + return nil, err + } } return stacks, nil } @@ -192,3 +200,75 @@ func (s *stackV1Beta2) List(opts metav1.ListOptions) ([]Stack, error) { func (s *stackV1Beta2) IsColliding(servicesClient corev1.ServiceInterface, st Stack) error { return nil } + +// stackV1Beta2 implements stackClient interface and talks to compose component v1beta2. +type stackV1Alpha3 struct { + stackV1Beta2OrHigherConverter + stacks composev1alpha3.StackInterface +} + +func newStackV1Alpha3(config *rest.Config, namespace string) (*stackV1Alpha3, error) { + client, err := composev1alpha3.NewForConfig(config) + if err != nil { + return nil, err + } + return &stackV1Alpha3{stacks: client.Stacks(namespace)}, nil +} + +func (s *stackV1Alpha3) CreateOrUpdate(internalStack Stack, childResources []childResource) error { + var ( + stack *v1alpha3.Stack + err error + ) + resolved := stackToV1alpha3(internalStack) + if stack, err = s.stacks.Get(internalStack.Name, metav1.GetOptions{}); err == nil { + stack.Spec = resolved.Spec + stack, err = s.stacks.Update(stack) + } else { + // Or create it + stack, err = s.stacks.Create(resolved) + } + if err != nil { + deleteChildResources(childResources) + return err + } + blockOwnerDeletion := true + isController := true + return setChildResourcesOwner(childResources, metav1.OwnerReference{ + APIVersion: v1alpha3.SchemeGroupVersion.String(), + Kind: "Stack", + Name: stack.Name, + UID: stack.UID, + BlockOwnerDeletion: &blockOwnerDeletion, + Controller: &isController, + }) +} + +func (s *stackV1Alpha3) Delete(name string) error { + return s.stacks.Delete(name, &metav1.DeleteOptions{}) +} + +func (s *stackV1Alpha3) Get(name string) (Stack, error) { + stackAlpha3, err := s.stacks.Get(name, metav1.GetOptions{}) + if err != nil { + return Stack{}, err + } + return stackFromV1alpha3(stackAlpha3), nil +} + +func (s *stackV1Alpha3) List(opts metav1.ListOptions) ([]Stack, error) { + list, err := s.stacks.List(opts) + if err != nil { + return nil, err + } + stacks := make([]Stack, len(list.Items)) + for i := range list.Items { + stacks[i] = stackFromV1alpha3(&list.Items[i]) + } + return stacks, nil +} + +// IsColliding is handle server side with the compose api v1beta2, so nothing to do here +func (s *stackV1Alpha3) IsColliding(servicesClient corev1.ServiceInterface, st Stack) error { + return nil +} diff --git a/cli/command/system/version.go b/cli/command/system/version.go index b6c7db30cf..a8f0beb1fd 100644 --- a/cli/command/system/version.go +++ b/cli/command/system/version.go @@ -12,8 +12,8 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" kubecontext "github.com/docker/cli/cli/context/kubernetes" + "github.com/docker/cli/kubernetes" "github.com/docker/cli/templates" - kubernetes "github.com/docker/compose-on-kubernetes/api" "github.com/docker/docker/api/types" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -260,13 +260,13 @@ func getKubernetesVersion(dockerCli command.Cli, kubeConfig string) *kubernetesV logrus.Debugf("failed to get Kubernetes client: %s", err) return &version } - version.StackAPI = getStackVersion(kubeClient) + version.StackAPI = getStackVersion(kubeClient, dockerCli.ClientInfo().HasExperimental) version.Kubernetes = getKubernetesServerVersion(kubeClient) return &version } -func getStackVersion(client *kubernetesClient.Clientset) string { - apiVersion, err := kubernetes.GetStackAPIVersion(client) +func getStackVersion(client *kubernetesClient.Clientset, experimental bool) string { + apiVersion, err := kubernetes.GetStackAPIVersion(client, experimental) if err != nil { logrus.Debugf("failed to get Stack API version: %s", err) return "Unknown" diff --git a/kubernetes/check.go b/kubernetes/check.go index 8d347280fd..6a676fa1dc 100644 --- a/kubernetes/check.go +++ b/kubernetes/check.go @@ -1,20 +1,60 @@ package kubernetes -import api "github.com/docker/compose-on-kubernetes/api" +import ( + apiv1alpha3 "github.com/docker/compose-on-kubernetes/api/compose/v1alpha3" + apiv1beta1 "github.com/docker/compose-on-kubernetes/api/compose/v1beta1" + apiv1beta2 "github.com/docker/compose-on-kubernetes/api/compose/v1beta2" + "github.com/pkg/errors" + apimachinerymetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" +) // StackVersion represents the detected Compose Component on Kubernetes side. -// Deprecated: Use github.com/docker/compose-on-kubernetes/api.StackVersion instead -type StackVersion = api.StackVersion +type StackVersion string const ( // StackAPIV1Beta1 is returned if it's the most recent version available. - // Deprecated: Use github.com/docker/compose-on-kubernetes/api.StackAPIV1Beta1 instead - StackAPIV1Beta1 = api.StackAPIV1Beta1 + StackAPIV1Beta1 = StackVersion("v1beta1") // StackAPIV1Beta2 is returned if it's the most recent version available. - // Deprecated: Use github.com/docker/compose-on-kubernetes/api.StackAPIV1Beta2 instead - StackAPIV1Beta2 = api.StackAPIV1Beta2 + StackAPIV1Beta2 = StackVersion("v1beta2") + // StackAPIV1Alpha3 is returned if it's the most recent version available, and experimental flag is on. + StackAPIV1Alpha3 = StackVersion("v1alpha3") ) -// GetStackAPIVersion returns the most recent stack API installed. -// Deprecated: Use github.com/docker/compose-on-kubernetes/api.GetStackAPIVersion instead -var GetStackAPIVersion = api.GetStackAPIVersion +// GetStackAPIVersion returns the most appropriate stack API version installed. +func GetStackAPIVersion(serverGroups discovery.ServerGroupsInterface, experimental bool) (StackVersion, error) { + groups, err := serverGroups.ServerGroups() + if err != nil { + return "", err + } + + return getAPIVersion(groups, experimental) +} + +func getAPIVersion(groups *metav1.APIGroupList, experimental bool) (StackVersion, error) { + switch { + case experimental && findVersion(apiv1alpha3.SchemeGroupVersion, groups.Groups): + return StackAPIV1Alpha3, nil + case findVersion(apiv1beta2.SchemeGroupVersion, groups.Groups): + return StackAPIV1Beta2, nil + case findVersion(apiv1beta1.SchemeGroupVersion, groups.Groups): + return StackAPIV1Beta1, nil + default: + return "", errors.New("failed to find a Stack API version") + } +} + +func findVersion(stackAPI schema.GroupVersion, groups []apimachinerymetav1.APIGroup) bool { + for _, group := range groups { + if group.Name == stackAPI.Group { + for _, version := range group.Versions { + if version.Version == stackAPI.Version { + return true + } + } + } + } + return false +} diff --git a/kubernetes/check_test.go b/kubernetes/check_test.go new file mode 100644 index 0000000000..28a9029f06 --- /dev/null +++ b/kubernetes/check_test.go @@ -0,0 +1,54 @@ +package kubernetes + +import ( + "testing" + + "gotest.tools/assert" + is "gotest.tools/assert/cmp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestGetStackAPIVersion(t *testing.T) { + var tests = []struct { + description string + groups *metav1.APIGroupList + experimental bool + err bool + expectedStack StackVersion + }{ + {"no stack api", makeGroups(), false, true, ""}, + {"v1beta1", makeGroups(groupVersion{"compose.docker.com", []string{"v1beta1"}}), false, false, StackAPIV1Beta1}, + {"v1beta2", makeGroups(groupVersion{"compose.docker.com", []string{"v1beta2"}}), false, false, StackAPIV1Beta2}, + {"most recent has precedence", makeGroups(groupVersion{"compose.docker.com", []string{"v1beta1", "v1beta2"}}), false, false, StackAPIV1Beta2}, + {"most recent has precedence", makeGroups(groupVersion{"compose.docker.com", []string{"v1beta1", "v1beta2", "v1alpha3"}}), false, false, StackAPIV1Beta2}, + {"most recent has precedence", makeGroups(groupVersion{"compose.docker.com", []string{"v1beta1", "v1beta2", "v1alpha3"}}), true, false, StackAPIV1Alpha3}, + } + + for _, test := range tests { + version, err := getAPIVersion(test.groups, test.experimental) + if test.err { + assert.ErrorContains(t, err, "") + } else { + assert.NilError(t, err) + } + assert.Check(t, is.Equal(test.expectedStack, version)) + } +} + +type groupVersion struct { + name string + versions []string +} + +func makeGroups(versions ...groupVersion) *metav1.APIGroupList { + groups := make([]metav1.APIGroup, len(versions)) + for i := range versions { + groups[i].Name = versions[i].name + for _, v := range versions[i].versions { + groups[i].Versions = append(groups[i].Versions, metav1.GroupVersionForDiscovery{Version: v}) + } + } + return &metav1.APIGroupList{ + Groups: groups, + } +}