diff --git a/cli/command/stack/kubernetes/convert.go b/cli/command/stack/kubernetes/convert.go index aa63daf276..b122090fb6 100644 --- a/cli/command/stack/kubernetes/convert.go +++ b/cli/command/stack/kubernetes/convert.go @@ -8,13 +8,72 @@ import ( "strings" "github.com/docker/cli/cli/compose/loader" + "github.com/docker/cli/cli/compose/schema" composeTypes "github.com/docker/cli/cli/compose/types" composetypes "github.com/docker/cli/cli/compose/types" "github.com/docker/cli/kubernetes/compose/v1beta1" "github.com/docker/cli/kubernetes/compose/v1beta2" + "github.com/pkg/errors" + yaml "gopkg.in/yaml.v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +// 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": + return stackV1Beta2Converter{}, nil + default: + return nil, errors.Errorf("stack version %s unsupported", version) + } +} + +// StackConverter converts a compose types.Config to a Stack +type StackConverter interface { + FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) +} + +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) + if err != nil { + return Stack{}, err + } + res, err := yaml.Marshal(cfg) + if err != nil { + return Stack{}, err + } + // reload the result to check that it produced a valid 3.5 compose file + resparsedConfig, err := loader.ParseYAML(res) + if err != nil { + return Stack{}, err + } + if err = schema.Validate(resparsedConfig, v1beta1.MaxComposeVersion); err != nil { + return Stack{}, errors.Wrapf(err, "the compose yaml file is invalid with v%s", v1beta1.MaxComposeVersion) + } + + st.ComposeFile = string(res) + return st, nil +} + +type stackV1Beta2Converter struct{} + +func (s stackV1Beta2Converter) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) { + return fromCompose(stderr, name, cfg) +} + +func fromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) { + return Stack{ + Name: name, + Spec: fromComposeConfig(stderr, cfg), + }, nil +} + func loadStackData(composefile string) (*composetypes.Config, error) { parsed, err := loader.ParseYAML([]byte(composefile)) if err != nil { @@ -30,44 +89,44 @@ func loadStackData(composefile string) (*composetypes.Config, error) { } // Conversions from internal stack to different stack compose component versions. -func stackFromV1beta1(in *v1beta1.Stack) (stack, error) { +func stackFromV1beta1(in *v1beta1.Stack) (Stack, error) { cfg, err := loadStackData(in.Spec.ComposeFile) if err != nil { - return stack{}, err + return Stack{}, err } - return stack{ - name: in.ObjectMeta.Name, - namespace: in.ObjectMeta.Namespace, - composeFile: in.Spec.ComposeFile, - spec: fromComposeConfig(ioutil.Discard, cfg), + return Stack{ + Name: in.ObjectMeta.Name, + Namespace: in.ObjectMeta.Namespace, + ComposeFile: in.Spec.ComposeFile, + Spec: fromComposeConfig(ioutil.Discard, cfg), }, nil } -func stackToV1beta1(s stack) *v1beta1.Stack { +func stackToV1beta1(s Stack) *v1beta1.Stack { return &v1beta1.Stack{ ObjectMeta: metav1.ObjectMeta{ - Name: s.name, + Name: s.Name, }, Spec: v1beta1.StackSpec{ - ComposeFile: s.composeFile, + ComposeFile: s.ComposeFile, }, } } -func stackFromV1beta2(in *v1beta2.Stack) stack { - return stack{ - name: in.ObjectMeta.Name, - namespace: in.ObjectMeta.Namespace, - spec: in.Spec, +func stackFromV1beta2(in *v1beta2.Stack) Stack { + return Stack{ + Name: in.ObjectMeta.Name, + Namespace: in.ObjectMeta.Namespace, + Spec: in.Spec, } } -func stackToV1beta2(s stack) *v1beta2.Stack { +func stackToV1beta2(s Stack) *v1beta2.Stack { return &v1beta2.Stack{ ObjectMeta: metav1.ObjectMeta{ - Name: s.name, + Name: s.Name, }, - Spec: s.spec, + Spec: s.Spec, } } diff --git a/cli/command/stack/kubernetes/convert_test.go b/cli/command/stack/kubernetes/convert_test.go new file mode 100644 index 0000000000..5e0f8dc54d --- /dev/null +++ b/cli/command/stack/kubernetes/convert_test.go @@ -0,0 +1,18 @@ +package kubernetes + +import ( + "testing" + + "gotest.tools/assert" + is "gotest.tools/assert/cmp" +) + +func TestNewStackConverter(t *testing.T) { + _, err := NewStackConverter("v1alpha1") + assert.Check(t, is.ErrorContains(err, "stack version v1alpha1 unsupported")) + + _, err = NewStackConverter("v1beta1") + assert.NilError(t, err) + _, err = NewStackConverter("v1beta2") + assert.NilError(t, err) +} diff --git a/cli/command/stack/kubernetes/deploy.go b/cli/command/stack/kubernetes/deploy.go index 501e0a240b..94776cf03c 100644 --- a/cli/command/stack/kubernetes/deploy.go +++ b/cli/command/stack/kubernetes/deploy.go @@ -75,13 +75,13 @@ func RunDeploy(dockerCli *KubeCli, opts options.Deploy, cfg *composetypes.Config } }() - err = watcher.Watch(stack.name, stack.getServices(), statusUpdates) + err = watcher.Watch(stack.Name, stack.getServices(), statusUpdates) close(statusUpdates) <-displayDone if err != nil { return err } - fmt.Fprintf(cmdOut, "\nStack %s is stable and running\n\n", stack.name) + fmt.Fprintf(cmdOut, "\nStack %s is stable and running\n\n", stack.Name) return nil } diff --git a/cli/command/stack/kubernetes/list.go b/cli/command/stack/kubernetes/list.go index f6efd050aa..240f4aa041 100644 --- a/cli/command/stack/kubernetes/list.go +++ b/cli/command/stack/kubernetes/list.go @@ -48,10 +48,10 @@ func getStacks(kubeCli *KubeCli, opts options.List) ([]*formatter.Stack, error) var formattedStacks []*formatter.Stack for _, stack := range stacks { formattedStacks = append(formattedStacks, &formatter.Stack{ - Name: stack.name, + Name: stack.Name, Services: len(stack.getServices()), Orchestrator: "Kubernetes", - Namespace: stack.namespace, + Namespace: stack.Namespace, }) } return formattedStacks, nil diff --git a/cli/command/stack/kubernetes/stack.go b/cli/command/stack/kubernetes/stack.go index 566e772924..67d1af689f 100644 --- a/cli/command/stack/kubernetes/stack.go +++ b/cli/command/stack/kubernetes/stack.go @@ -12,18 +12,18 @@ import ( corev1 "k8s.io/client-go/kubernetes/typed/core/v1" ) -// stack is the main type used by stack commands so they remain independent from kubernetes compose component version. -type stack struct { - name string - namespace string - composeFile string - spec *v1beta2.StackSpec +// Stack is the main type used by stack commands so they remain independent from kubernetes compose component version. +type Stack struct { + Name string + Namespace string + ComposeFile string + Spec *v1beta2.StackSpec } // getServices returns all the stack service names, sorted lexicographically -func (s *stack) getServices() []string { - services := make([]string, len(s.spec.Services)) - for i, service := range s.spec.Services { +func (s *Stack) getServices() []string { + services := make([]string, len(s.Spec.Services)) + for i, service := range s.Spec.Services { services[i] = service.Name } sort.Strings(services) @@ -31,8 +31,8 @@ func (s *stack) getServices() []string { } // createFileBasedConfigMaps creates a Kubernetes ConfigMap for each Compose global file-based config. -func (s *stack) createFileBasedConfigMaps(configMaps corev1.ConfigMapInterface) error { - for name, config := range s.spec.Configs { +func (s *Stack) createFileBasedConfigMaps(configMaps corev1.ConfigMapInterface) error { + for name, config := range s.Spec.Configs { if config.File == "" { continue } @@ -43,7 +43,7 @@ func (s *stack) createFileBasedConfigMaps(configMaps corev1.ConfigMapInterface) return err } - if _, err := configMaps.Create(toConfigMap(s.name, name, fileName, content)); err != nil { + if _, err := configMaps.Create(toConfigMap(s.Name, name, fileName, content)); err != nil { return err } } @@ -71,8 +71,8 @@ func toConfigMap(stackName, name, key string, content []byte) *apiv1.ConfigMap { } // createFileBasedSecrets creates a Kubernetes Secret for each Compose global file-based secret. -func (s *stack) createFileBasedSecrets(secrets corev1.SecretInterface) error { - for name, secret := range s.spec.Secrets { +func (s *Stack) createFileBasedSecrets(secrets corev1.SecretInterface) error { + for name, secret := range s.Spec.Secrets { if secret.File == "" { continue } @@ -83,7 +83,7 @@ func (s *stack) createFileBasedSecrets(secrets corev1.SecretInterface) error { return err } - if _, err := secrets.Create(toSecret(s.name, name, fileName, content)); err != nil { + if _, err := secrets.Create(toSecret(s.Name, name, fileName, content)); err != nil { return err } } diff --git a/cli/command/stack/kubernetes/stackclient.go b/cli/command/stack/kubernetes/stackclient.go index b59a50d476..e769e7466d 100644 --- a/cli/command/stack/kubernetes/stackclient.go +++ b/cli/command/stack/kubernetes/stackclient.go @@ -2,17 +2,10 @@ package kubernetes import ( "fmt" - "io" - "github.com/docker/cli/cli/compose/loader" - "github.com/docker/cli/cli/compose/schema" - composetypes "github.com/docker/cli/cli/compose/types" composev1beta1 "github.com/docker/cli/kubernetes/client/clientset/typed/compose/v1beta1" composev1beta2 "github.com/docker/cli/kubernetes/client/clientset/typed/compose/v1beta2" - v1beta1types "github.com/docker/cli/kubernetes/compose/v1beta1" "github.com/docker/cli/kubernetes/labels" - "github.com/pkg/errors" - yaml "gopkg.in/yaml.v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/rest" @@ -20,16 +13,17 @@ import ( // StackClient talks to a kubernetes compose component. type StackClient interface { - CreateOrUpdate(s stack) error + StackConverter + CreateOrUpdate(s Stack) error Delete(name string) error - Get(name string) (stack, error) - List(opts metav1.ListOptions) ([]stack, error) - IsColliding(servicesClient corev1.ServiceInterface, s stack) error - FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (stack, error) + Get(name string) (Stack, error) + List(opts metav1.ListOptions) ([]Stack, error) + IsColliding(servicesClient corev1.ServiceInterface, s Stack) error } // stackV1Beta1 implements stackClient interface and talks to compose component v1beta1. type stackV1Beta1 struct { + stackV1Beta1Converter stacks composev1beta1.StackInterface } @@ -41,10 +35,10 @@ func newStackV1Beta1(config *rest.Config, namespace string) (*stackV1Beta1, erro return &stackV1Beta1{stacks: client.Stacks(namespace)}, nil } -func (s *stackV1Beta1) CreateOrUpdate(internalStack stack) error { +func (s *stackV1Beta1) CreateOrUpdate(internalStack Stack) error { // If it already exists, update the stack - if stackBeta1, err := s.stacks.Get(internalStack.name, metav1.GetOptions{}); err == nil { - stackBeta1.Spec.ComposeFile = internalStack.composeFile + if stackBeta1, err := s.stacks.Get(internalStack.Name, metav1.GetOptions{}); err == nil { + stackBeta1.Spec.ComposeFile = internalStack.ComposeFile _, err := s.stacks.Update(stackBeta1) return err } @@ -57,20 +51,20 @@ func (s *stackV1Beta1) Delete(name string) error { return s.stacks.Delete(name, &metav1.DeleteOptions{}) } -func (s *stackV1Beta1) Get(name string) (stack, error) { +func (s *stackV1Beta1) Get(name string) (Stack, error) { stackBeta1, err := s.stacks.Get(name, metav1.GetOptions{}) if err != nil { - return stack{}, err + return Stack{}, err } return stackFromV1beta1(stackBeta1) } -func (s *stackV1Beta1) List(opts metav1.ListOptions) ([]stack, error) { +func (s *stackV1Beta1) List(opts metav1.ListOptions) ([]Stack, error) { list, err := s.stacks.List(opts) if err != nil { return nil, err } - stacks := make([]stack, len(list.Items)) + stacks := make([]Stack, len(list.Items)) for i := range list.Items { stack, err := stackFromV1beta1(&list.Items[i]) if err != nil { @@ -82,9 +76,9 @@ func (s *stackV1Beta1) List(opts metav1.ListOptions) ([]stack, error) { } // IsColliding verifies that services defined in the stack collides with already deployed services -func (s *stackV1Beta1) IsColliding(servicesClient corev1.ServiceInterface, st stack) error { +func (s *stackV1Beta1) IsColliding(servicesClient corev1.ServiceInterface, st Stack) error { for _, srv := range st.getServices() { - if err := verify(servicesClient, st.name, srv); err != nil { + if err := verify(servicesClient, st.Name, srv); err != nil { return err } } @@ -108,31 +102,9 @@ func verify(services corev1.ServiceInterface, stackName string, service string) return nil } -func (s *stackV1Beta1) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (stack, error) { - cfg.Version = v1beta1types.MaxComposeVersion - st, err := fromCompose(stderr, name, cfg) - if err != nil { - return stack{}, err - } - res, err := yaml.Marshal(cfg) - if err != nil { - return stack{}, err - } - // reload the result to check that it produced a valid 3.5 compose file - resparsedConfig, err := loader.ParseYAML(res) - if err != nil { - return stack{}, err - } - if err = schema.Validate(resparsedConfig, v1beta1types.MaxComposeVersion); err != nil { - return stack{}, errors.Wrapf(err, "the compose yaml file is invalid with v%s", v1beta1types.MaxComposeVersion) - } - - st.composeFile = string(res) - return st, nil -} - // stackV1Beta2 implements stackClient interface and talks to compose component v1beta2. type stackV1Beta2 struct { + stackV1Beta2Converter stacks composev1beta2.StackInterface } @@ -144,10 +116,10 @@ func newStackV1Beta2(config *rest.Config, namespace string) (*stackV1Beta2, erro return &stackV1Beta2{stacks: client.Stacks(namespace)}, nil } -func (s *stackV1Beta2) CreateOrUpdate(internalStack stack) error { +func (s *stackV1Beta2) CreateOrUpdate(internalStack Stack) error { // If it already exists, update the stack - if stackBeta2, err := s.stacks.Get(internalStack.name, metav1.GetOptions{}); err == nil { - stackBeta2.Spec = internalStack.spec + if stackBeta2, err := s.stacks.Get(internalStack.Name, metav1.GetOptions{}); err == nil { + stackBeta2.Spec = internalStack.Spec _, err := s.stacks.Update(stackBeta2) return err } @@ -160,20 +132,20 @@ func (s *stackV1Beta2) Delete(name string) error { return s.stacks.Delete(name, &metav1.DeleteOptions{}) } -func (s *stackV1Beta2) Get(name string) (stack, error) { +func (s *stackV1Beta2) Get(name string) (Stack, error) { stackBeta2, err := s.stacks.Get(name, metav1.GetOptions{}) if err != nil { - return stack{}, err + return Stack{}, err } return stackFromV1beta2(stackBeta2), nil } -func (s *stackV1Beta2) List(opts metav1.ListOptions) ([]stack, error) { +func (s *stackV1Beta2) List(opts metav1.ListOptions) ([]Stack, error) { list, err := s.stacks.List(opts) if err != nil { return nil, err } - stacks := make([]stack, len(list.Items)) + stacks := make([]Stack, len(list.Items)) for i := range list.Items { stacks[i] = stackFromV1beta2(&list.Items[i]) } @@ -181,17 +153,6 @@ func (s *stackV1Beta2) List(opts metav1.ListOptions) ([]stack, error) { } // IsColliding is handle server side with the compose api v1beta2, so nothing to do here -func (s *stackV1Beta2) IsColliding(servicesClient corev1.ServiceInterface, st stack) error { +func (s *stackV1Beta2) IsColliding(servicesClient corev1.ServiceInterface, st Stack) error { return nil } - -func (s *stackV1Beta2) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (stack, error) { - return fromCompose(stderr, name, cfg) -} - -func fromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (stack, error) { - return stack{ - name: name, - spec: fromComposeConfig(stderr, cfg), - }, nil -} diff --git a/cli/command/stack/kubernetes/stackclient_test.go b/cli/command/stack/kubernetes/stackclient_test.go index 8757f1c466..da4a3426aa 100644 --- a/cli/command/stack/kubernetes/stackclient_test.go +++ b/cli/command/stack/kubernetes/stackclient_test.go @@ -25,7 +25,7 @@ func TestFromCompose(t *testing.T) { }, }) assert.NilError(t, err) - assert.Equal(t, "foo", s.name) + assert.Equal(t, "foo", s.Name) assert.Equal(t, string(`version: "3.5" services: bar: @@ -36,7 +36,7 @@ networks: {} volumes: {} secrets: {} configs: {} -`), s.composeFile) +`), s.ComposeFile) } func TestFromComposeUnsupportedVersion(t *testing.T) {