Merge pull request #1152 from vdemeester/extract-converter

Extract StackConverter from the StackClient
This commit is contained in:
Sebastiaan van Stijn 2018-06-29 13:36:55 +02:00 committed by GitHub
commit 7c7c299eee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 140 additions and 102 deletions

View File

@ -8,13 +8,72 @@ import (
"strings" "strings"
"github.com/docker/cli/cli/compose/loader" "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"
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/v1beta1"
"github.com/docker/cli/kubernetes/compose/v1beta2" "github.com/docker/cli/kubernetes/compose/v1beta2"
"github.com/pkg/errors"
yaml "gopkg.in/yaml.v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 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) { func loadStackData(composefile string) (*composetypes.Config, error) {
parsed, err := loader.ParseYAML([]byte(composefile)) parsed, err := loader.ParseYAML([]byte(composefile))
if err != nil { if err != nil {
@ -30,44 +89,44 @@ func loadStackData(composefile string) (*composetypes.Config, error) {
} }
// Conversions from internal stack to different stack compose component versions. // 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) cfg, err := loadStackData(in.Spec.ComposeFile)
if err != nil { if err != nil {
return stack{}, err 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: fromComposeConfig(ioutil.Discard, cfg),
}, nil }, nil
} }
func stackToV1beta1(s stack) *v1beta1.Stack { func stackToV1beta1(s Stack) *v1beta1.Stack {
return &v1beta1.Stack{ return &v1beta1.Stack{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: s.name, Name: s.Name,
}, },
Spec: v1beta1.StackSpec{ Spec: v1beta1.StackSpec{
ComposeFile: s.composeFile, ComposeFile: s.ComposeFile,
}, },
} }
} }
func stackFromV1beta2(in *v1beta2.Stack) stack { func stackFromV1beta2(in *v1beta2.Stack) Stack {
return stack{ return Stack{
name: in.ObjectMeta.Name, Name: in.ObjectMeta.Name,
namespace: in.ObjectMeta.Namespace, Namespace: in.ObjectMeta.Namespace,
spec: in.Spec, Spec: in.Spec,
} }
} }
func stackToV1beta2(s stack) *v1beta2.Stack { func stackToV1beta2(s Stack) *v1beta2.Stack {
return &v1beta2.Stack{ return &v1beta2.Stack{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: s.name, Name: s.Name,
}, },
Spec: s.spec, Spec: s.Spec,
} }
} }

View File

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

View File

@ -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) close(statusUpdates)
<-displayDone <-displayDone
if err != nil { if err != nil {
return err 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 return nil
} }

View File

@ -48,10 +48,10 @@ func getStacks(kubeCli *KubeCli, opts options.List) ([]*formatter.Stack, error)
var formattedStacks []*formatter.Stack var formattedStacks []*formatter.Stack
for _, stack := range stacks { for _, stack := range stacks {
formattedStacks = append(formattedStacks, &formatter.Stack{ formattedStacks = append(formattedStacks, &formatter.Stack{
Name: stack.name, Name: stack.Name,
Services: len(stack.getServices()), Services: len(stack.getServices()),
Orchestrator: "Kubernetes", Orchestrator: "Kubernetes",
Namespace: stack.namespace, Namespace: stack.Namespace,
}) })
} }
return formattedStacks, nil return formattedStacks, nil

View File

@ -12,18 +12,18 @@ import (
corev1 "k8s.io/client-go/kubernetes/typed/core/v1" 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. // Stack is the main type used by stack commands so they remain independent from kubernetes compose component version.
type stack struct { type Stack struct {
name string Name string
namespace string Namespace string
composeFile string ComposeFile string
spec *v1beta2.StackSpec Spec *v1beta2.StackSpec
} }
// getServices returns all the stack service names, sorted lexicographically // getServices returns all the stack service names, sorted lexicographically
func (s *stack) getServices() []string { func (s *Stack) getServices() []string {
services := make([]string, len(s.spec.Services)) services := make([]string, len(s.Spec.Services))
for i, service := range s.spec.Services { for i, service := range s.Spec.Services {
services[i] = service.Name services[i] = service.Name
} }
sort.Strings(services) sort.Strings(services)
@ -31,8 +31,8 @@ func (s *stack) getServices() []string {
} }
// createFileBasedConfigMaps creates a Kubernetes ConfigMap for each Compose global file-based config. // createFileBasedConfigMaps creates a Kubernetes ConfigMap for each Compose global file-based config.
func (s *stack) createFileBasedConfigMaps(configMaps corev1.ConfigMapInterface) error { func (s *Stack) createFileBasedConfigMaps(configMaps corev1.ConfigMapInterface) error {
for name, config := range s.spec.Configs { for name, config := range s.Spec.Configs {
if config.File == "" { if config.File == "" {
continue continue
} }
@ -43,7 +43,7 @@ func (s *stack) createFileBasedConfigMaps(configMaps corev1.ConfigMapInterface)
return err 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 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. // createFileBasedSecrets creates a Kubernetes Secret for each Compose global file-based secret.
func (s *stack) createFileBasedSecrets(secrets corev1.SecretInterface) error { func (s *Stack) createFileBasedSecrets(secrets corev1.SecretInterface) error {
for name, secret := range s.spec.Secrets { for name, secret := range s.Spec.Secrets {
if secret.File == "" { if secret.File == "" {
continue continue
} }
@ -83,7 +83,7 @@ func (s *stack) createFileBasedSecrets(secrets corev1.SecretInterface) error {
return err 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 return err
} }
} }

View File

@ -2,17 +2,10 @@ package kubernetes
import ( import (
"fmt" "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" composev1beta1 "github.com/docker/cli/kubernetes/client/clientset/typed/compose/v1beta1"
composev1beta2 "github.com/docker/cli/kubernetes/client/clientset/typed/compose/v1beta2" 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/docker/cli/kubernetes/labels"
"github.com/pkg/errors"
yaml "gopkg.in/yaml.v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1" corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
@ -20,16 +13,17 @@ import (
// StackClient talks to a kubernetes compose component. // StackClient talks to a kubernetes compose component.
type StackClient interface { type StackClient interface {
CreateOrUpdate(s stack) error StackConverter
CreateOrUpdate(s Stack) error
Delete(name string) error Delete(name string) error
Get(name string) (stack, error) Get(name string) (Stack, error)
List(opts metav1.ListOptions) ([]stack, error) List(opts metav1.ListOptions) ([]Stack, error)
IsColliding(servicesClient corev1.ServiceInterface, s stack) error IsColliding(servicesClient corev1.ServiceInterface, s Stack) error
FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (stack, error)
} }
// stackV1Beta1 implements stackClient interface and talks to compose component v1beta1. // stackV1Beta1 implements stackClient interface and talks to compose component v1beta1.
type stackV1Beta1 struct { type stackV1Beta1 struct {
stackV1Beta1Converter
stacks composev1beta1.StackInterface stacks composev1beta1.StackInterface
} }
@ -41,10 +35,10 @@ func newStackV1Beta1(config *rest.Config, namespace string) (*stackV1Beta1, erro
return &stackV1Beta1{stacks: client.Stacks(namespace)}, nil 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 it already exists, update the stack
if stackBeta1, err := s.stacks.Get(internalStack.name, metav1.GetOptions{}); err == nil { if stackBeta1, err := s.stacks.Get(internalStack.Name, metav1.GetOptions{}); err == nil {
stackBeta1.Spec.ComposeFile = internalStack.composeFile stackBeta1.Spec.ComposeFile = internalStack.ComposeFile
_, err := s.stacks.Update(stackBeta1) _, err := s.stacks.Update(stackBeta1)
return err return err
} }
@ -57,20 +51,20 @@ func (s *stackV1Beta1) Delete(name string) error {
return s.stacks.Delete(name, &metav1.DeleteOptions{}) 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{}) stackBeta1, err := s.stacks.Get(name, metav1.GetOptions{})
if err != nil { if err != nil {
return stack{}, err return Stack{}, err
} }
return stackFromV1beta1(stackBeta1) 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) list, err := s.stacks.List(opts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
stacks := make([]stack, len(list.Items)) stacks := make([]Stack, len(list.Items))
for i := range list.Items { for i := range list.Items {
stack, err := stackFromV1beta1(&list.Items[i]) stack, err := stackFromV1beta1(&list.Items[i])
if err != nil { 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 // 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() { 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 return err
} }
} }
@ -108,31 +102,9 @@ func verify(services corev1.ServiceInterface, stackName string, service string)
return nil 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. // stackV1Beta2 implements stackClient interface and talks to compose component v1beta2.
type stackV1Beta2 struct { type stackV1Beta2 struct {
stackV1Beta2Converter
stacks composev1beta2.StackInterface stacks composev1beta2.StackInterface
} }
@ -144,10 +116,10 @@ func newStackV1Beta2(config *rest.Config, namespace string) (*stackV1Beta2, erro
return &stackV1Beta2{stacks: client.Stacks(namespace)}, nil 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 it already exists, update the stack
if stackBeta2, err := s.stacks.Get(internalStack.name, metav1.GetOptions{}); err == nil { if stackBeta2, err := s.stacks.Get(internalStack.Name, metav1.GetOptions{}); err == nil {
stackBeta2.Spec = internalStack.spec stackBeta2.Spec = internalStack.Spec
_, err := s.stacks.Update(stackBeta2) _, err := s.stacks.Update(stackBeta2)
return err return err
} }
@ -160,20 +132,20 @@ func (s *stackV1Beta2) Delete(name string) error {
return s.stacks.Delete(name, &metav1.DeleteOptions{}) 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{}) stackBeta2, err := s.stacks.Get(name, metav1.GetOptions{})
if err != nil { if err != nil {
return stack{}, err return Stack{}, err
} }
return stackFromV1beta2(stackBeta2), nil 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) list, err := s.stacks.List(opts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
stacks := make([]stack, len(list.Items)) stacks := make([]Stack, len(list.Items))
for i := range list.Items { for i := range list.Items {
stacks[i] = stackFromV1beta2(&list.Items[i]) 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 // 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 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
}

View File

@ -25,7 +25,7 @@ func TestFromCompose(t *testing.T) {
}, },
}) })
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, "foo", s.name) assert.Equal(t, "foo", s.Name)
assert.Equal(t, string(`version: "3.5" assert.Equal(t, string(`version: "3.5"
services: services:
bar: bar:
@ -36,7 +36,7 @@ networks: {}
volumes: {} volumes: {}
secrets: {} secrets: {}
configs: {} configs: {}
`), s.composeFile) `), s.ComposeFile)
} }
func TestFromComposeUnsupportedVersion(t *testing.T) { func TestFromComposeUnsupportedVersion(t *testing.T) {