Add a common interface between different Kubernetes Stack API versions and use it in kubernetes stack commands
* Add kubernetes Stack API v1beta2 client
* Upgrade v1beta1 client to remove generated code
Signed-off-by: Silvin Lubecki <silvin.lubecki@docker.com>
Upstream-commit: f958c66a6d
Component: cli
This commit is contained in:
committed by
Mathieu Champlon
parent
c783effbb0
commit
de5526bad9
@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/kubernetes"
|
||||
composev1beta1 "github.com/docker/cli/kubernetes/client/clientset_generated/clientset/typed/compose/v1beta1"
|
||||
"github.com/docker/docker/pkg/homedir"
|
||||
"github.com/pkg/errors"
|
||||
flag "github.com/spf13/pflag"
|
||||
@ -77,20 +76,17 @@ func (c *KubeCli) composeClient() (*Factory, error) {
|
||||
return NewFactory(c.kubeNamespace, c.kubeConfig)
|
||||
}
|
||||
|
||||
func (c *KubeCli) stacks() (composev1beta1.StackInterface, error) {
|
||||
func (c *KubeCli) stacks() (stackClient, error) {
|
||||
version, err := kubernetes.GetStackAPIVersion(c.clientSet)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch version {
|
||||
case kubernetes.StackAPIV1Beta1:
|
||||
clientSet, err := composev1beta1.NewForConfig(c.kubeConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return clientSet.Stacks(c.kubeNamespace), nil
|
||||
return newStackV1Beta1(c.kubeConfig, c.kubeNamespace)
|
||||
case kubernetes.StackAPIV1Beta2:
|
||||
return newStackV1Beta2(c.kubeConfig, c.kubeNamespace)
|
||||
default:
|
||||
return nil, errors.Errorf("no supported Stack API version")
|
||||
}
|
||||
|
||||
@ -1,54 +0,0 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1"
|
||||
"github.com/docker/cli/kubernetes/labels"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
)
|
||||
|
||||
// IsColliding verify that services defined in the stack collides with already deployed services
|
||||
func IsColliding(services corev1.ServiceInterface, stack *apiv1beta1.Stack, cfg *composetypes.Config) error {
|
||||
stackObjects := getServices(cfg)
|
||||
|
||||
for _, srv := range stackObjects {
|
||||
if err := verify(services, stack.Name, srv); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// verify checks wether the service is already present in kubernetes.
|
||||
// If we find the service by name but it doesn't have our label or it has a different value
|
||||
// than the stack name for the label, we fail (i.e. it will collide)
|
||||
func verify(services corev1.ServiceInterface, stackName string, service string) error {
|
||||
svc, err := services.Get(service, metav1.GetOptions{})
|
||||
if err == nil {
|
||||
if key, ok := svc.ObjectMeta.Labels[labels.ForStackName]; ok {
|
||||
if key != stackName {
|
||||
return fmt.Errorf("service %s already present in stack named %s", service, key)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("service %s already present in the cluster", service)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getServices(cfg *composetypes.Config) []string {
|
||||
services := make([]string, len(cfg.Services))
|
||||
for i := range cfg.Services {
|
||||
services[i] = cfg.Services[i].Name
|
||||
}
|
||||
sort.Strings(services)
|
||||
return services
|
||||
}
|
||||
@ -1,41 +1,327 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/kubernetes/labels"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
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"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// toConfigMap converts a Compose Config to a Kube ConfigMap.
|
||||
func toConfigMap(stackName, name, key string, content []byte) *apiv1.ConfigMap {
|
||||
return &apiv1.ConfigMap{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Labels: map[string]string{
|
||||
labels.ForStackName: stackName,
|
||||
func loadStackData(composefile string) (*composetypes.Config, error) {
|
||||
parsed, err := loader.ParseYAML([]byte(composefile))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return loader.Load(composetypes.ConfigDetails{
|
||||
ConfigFiles: []composetypes.ConfigFile{
|
||||
{
|
||||
Config: parsed,
|
||||
},
|
||||
},
|
||||
Data: map[string]string{
|
||||
key: string(content),
|
||||
})
|
||||
}
|
||||
|
||||
// Conversions from internal stack to different stack compose component versions.
|
||||
func stackFromV1beta1(in *v1beta1.Stack) (stack, error) {
|
||||
cfg, err := loadStackData(in.Spec.ComposeFile)
|
||||
if err != nil {
|
||||
return stack{}, err
|
||||
}
|
||||
return stack{
|
||||
name: in.ObjectMeta.Name,
|
||||
composeFile: in.Spec.ComposeFile,
|
||||
spec: fromComposeConfig(cfg),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func stackToV1beta1(s stack) *v1beta1.Stack {
|
||||
return &v1beta1.Stack{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: s.name,
|
||||
},
|
||||
Spec: v1beta1.StackSpec{
|
||||
ComposeFile: s.composeFile,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// toSecret converts a Compose Secret to a Kube Secret.
|
||||
func toSecret(stackName, name, key string, content []byte) *apiv1.Secret {
|
||||
return &apiv1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Labels: map[string]string{
|
||||
labels.ForStackName: stackName,
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
key: content,
|
||||
},
|
||||
func stackFromV1beta2(in *v1beta2.Stack) stack {
|
||||
return stack{
|
||||
name: in.ObjectMeta.Name,
|
||||
spec: in.Spec,
|
||||
}
|
||||
}
|
||||
|
||||
func stackToV1beta2(s stack) *v1beta2.Stack {
|
||||
return &v1beta2.Stack{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: s.name,
|
||||
},
|
||||
Spec: s.spec,
|
||||
}
|
||||
}
|
||||
|
||||
func fromComposeConfig(c *composeTypes.Config) *v1beta2.StackSpec {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
serviceConfigs := make([]v1beta2.ServiceConfig, len(c.Services))
|
||||
for i, s := range c.Services {
|
||||
serviceConfigs[i] = fromComposeServiceConfig(s)
|
||||
}
|
||||
return &v1beta2.StackSpec{
|
||||
Services: serviceConfigs,
|
||||
Secrets: fromComposeSecrets(c.Secrets),
|
||||
Configs: fromComposeConfigs(c.Configs),
|
||||
}
|
||||
}
|
||||
|
||||
func fromComposeSecrets(s map[string]composeTypes.SecretConfig) map[string]v1beta2.SecretConfig {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
m := map[string]v1beta2.SecretConfig{}
|
||||
for key, value := range s {
|
||||
m[key] = v1beta2.SecretConfig{
|
||||
Name: value.Name,
|
||||
File: value.File,
|
||||
External: v1beta2.External{
|
||||
Name: value.External.Name,
|
||||
External: value.External.External,
|
||||
},
|
||||
Labels: value.Labels,
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func fromComposeConfigs(s map[string]composeTypes.ConfigObjConfig) map[string]v1beta2.ConfigObjConfig {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
m := map[string]v1beta2.ConfigObjConfig{}
|
||||
for key, value := range s {
|
||||
m[key] = v1beta2.ConfigObjConfig{
|
||||
Name: value.Name,
|
||||
File: value.File,
|
||||
External: v1beta2.External{
|
||||
Name: value.External.Name,
|
||||
External: value.External.External,
|
||||
},
|
||||
Labels: value.Labels,
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func fromComposeServiceConfig(s composeTypes.ServiceConfig) v1beta2.ServiceConfig {
|
||||
var userID *int64
|
||||
if s.User != "" {
|
||||
numerical, err := strconv.Atoi(s.User)
|
||||
if err == nil {
|
||||
unixUserID := int64(numerical)
|
||||
userID = &unixUserID
|
||||
}
|
||||
}
|
||||
return v1beta2.ServiceConfig{
|
||||
Name: s.Name,
|
||||
CapAdd: s.CapAdd,
|
||||
CapDrop: s.CapDrop,
|
||||
Command: s.Command,
|
||||
Configs: fromComposeServiceConfigs(s.Configs),
|
||||
Deploy: v1beta2.DeployConfig{
|
||||
Mode: s.Deploy.Mode,
|
||||
Replicas: s.Deploy.Replicas,
|
||||
Labels: s.Deploy.Labels,
|
||||
UpdateConfig: fromComposeUpdateConfig(s.Deploy.UpdateConfig),
|
||||
Resources: fromComposeResources(s.Deploy.Resources),
|
||||
RestartPolicy: fromComposeRestartPolicy(s.Deploy.RestartPolicy),
|
||||
Placement: fromComposePlacement(s.Deploy.Placement),
|
||||
},
|
||||
Entrypoint: s.Entrypoint,
|
||||
Environment: s.Environment,
|
||||
ExtraHosts: s.ExtraHosts,
|
||||
Hostname: s.Hostname,
|
||||
HealthCheck: fromComposeHealthcheck(s.HealthCheck),
|
||||
Image: s.Image,
|
||||
Ipc: s.Ipc,
|
||||
Labels: s.Labels,
|
||||
Pid: s.Pid,
|
||||
Ports: fromComposePorts(s.Ports),
|
||||
Privileged: s.Privileged,
|
||||
ReadOnly: s.ReadOnly,
|
||||
Secrets: fromComposeServiceSecrets(s.Secrets),
|
||||
StdinOpen: s.StdinOpen,
|
||||
StopGracePeriod: s.StopGracePeriod,
|
||||
Tmpfs: s.Tmpfs,
|
||||
Tty: s.Tty,
|
||||
User: userID,
|
||||
Volumes: fromComposeServiceVolumeConfig(s.Volumes),
|
||||
WorkingDir: s.WorkingDir,
|
||||
}
|
||||
}
|
||||
|
||||
func fromComposePorts(ports []composeTypes.ServicePortConfig) []v1beta2.ServicePortConfig {
|
||||
if ports == nil {
|
||||
return nil
|
||||
}
|
||||
p := make([]v1beta2.ServicePortConfig, len(ports))
|
||||
for i, port := range ports {
|
||||
p[i] = v1beta2.ServicePortConfig{
|
||||
Mode: port.Mode,
|
||||
Target: port.Target,
|
||||
Published: port.Published,
|
||||
Protocol: port.Protocol,
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func fromComposeServiceSecrets(secrets []composeTypes.ServiceSecretConfig) []v1beta2.ServiceSecretConfig {
|
||||
if secrets == nil {
|
||||
return nil
|
||||
}
|
||||
c := make([]v1beta2.ServiceSecretConfig, len(secrets))
|
||||
for i, secret := range secrets {
|
||||
c[i] = v1beta2.ServiceSecretConfig{
|
||||
Source: secret.Source,
|
||||
Target: secret.Target,
|
||||
UID: secret.UID,
|
||||
Mode: secret.Mode,
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func fromComposeServiceConfigs(configs []composeTypes.ServiceConfigObjConfig) []v1beta2.ServiceConfigObjConfig {
|
||||
if configs == nil {
|
||||
return nil
|
||||
}
|
||||
c := make([]v1beta2.ServiceConfigObjConfig, len(configs))
|
||||
for i, config := range configs {
|
||||
c[i] = v1beta2.ServiceConfigObjConfig{
|
||||
Source: config.Source,
|
||||
Target: config.Target,
|
||||
UID: config.UID,
|
||||
Mode: config.Mode,
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func fromComposeHealthcheck(h *composeTypes.HealthCheckConfig) *v1beta2.HealthCheckConfig {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
return &v1beta2.HealthCheckConfig{
|
||||
Test: h.Test,
|
||||
Timeout: h.Timeout,
|
||||
Interval: h.Interval,
|
||||
Retries: h.Retries,
|
||||
}
|
||||
}
|
||||
|
||||
func fromComposePlacement(p composeTypes.Placement) v1beta2.Placement {
|
||||
return v1beta2.Placement{
|
||||
Constraints: fromComposeConstraints(p.Constraints),
|
||||
}
|
||||
}
|
||||
|
||||
var constraintEquals = regexp.MustCompile(`([\w\.]*)\W*(==|!=)\W*([\w\.]*)`)
|
||||
|
||||
const (
|
||||
swarmOs = "node.platform.os"
|
||||
swarmArch = "node.platform.arch"
|
||||
swarmHostname = "node.hostname"
|
||||
swarmLabelPrefix = "node.labels."
|
||||
)
|
||||
|
||||
func fromComposeConstraints(s []string) *v1beta2.Constraints {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
constraints := &v1beta2.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{
|
||||
Operator: operator,
|
||||
Value: value,
|
||||
}
|
||||
switch {
|
||||
case key == swarmOs:
|
||||
constraints.OperatingSystem = constraint
|
||||
case key == swarmArch:
|
||||
constraints.Architecture = constraint
|
||||
case key == swarmHostname:
|
||||
constraints.Hostname = constraint
|
||||
case strings.HasPrefix(key, swarmLabelPrefix):
|
||||
if constraints.MatchLabels == nil {
|
||||
constraints.MatchLabels = map[string]v1beta2.Constraint{}
|
||||
}
|
||||
constraints.MatchLabels[strings.TrimPrefix(key, swarmLabelPrefix)] = *constraint
|
||||
}
|
||||
}
|
||||
}
|
||||
return constraints
|
||||
}
|
||||
|
||||
func fromComposeResources(r composeTypes.Resources) v1beta2.Resources {
|
||||
return v1beta2.Resources{
|
||||
Limits: fromComposeResourcesResource(r.Limits),
|
||||
Reservations: fromComposeResourcesResource(r.Reservations),
|
||||
}
|
||||
}
|
||||
|
||||
func fromComposeResourcesResource(r *composeTypes.Resource) *v1beta2.Resource {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return &v1beta2.Resource{
|
||||
MemoryBytes: int64(r.MemoryBytes),
|
||||
NanoCPUs: r.NanoCPUs,
|
||||
}
|
||||
}
|
||||
|
||||
func fromComposeUpdateConfig(u *composeTypes.UpdateConfig) *v1beta2.UpdateConfig {
|
||||
if u == nil {
|
||||
return nil
|
||||
}
|
||||
return &v1beta2.UpdateConfig{
|
||||
Parallelism: u.Parallelism,
|
||||
}
|
||||
}
|
||||
|
||||
func fromComposeRestartPolicy(r *composeTypes.RestartPolicy) *v1beta2.RestartPolicy {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return &v1beta2.RestartPolicy{
|
||||
Condition: r.Condition,
|
||||
}
|
||||
}
|
||||
|
||||
func fromComposeServiceVolumeConfig(vs []composeTypes.ServiceVolumeConfig) []v1beta2.ServiceVolumeConfig {
|
||||
if vs == nil {
|
||||
return nil
|
||||
}
|
||||
volumes := []v1beta2.ServiceVolumeConfig{}
|
||||
for _, v := range vs {
|
||||
volumes = append(volumes, v1beta2.ServiceVolumeConfig{
|
||||
Type: v.Type,
|
||||
Source: v.Source,
|
||||
Target: v.Target,
|
||||
ReadOnly: v.ReadOnly,
|
||||
})
|
||||
}
|
||||
return volumes
|
||||
}
|
||||
|
||||
@ -2,15 +2,10 @@ package kubernetes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
|
||||
"github.com/docker/cli/cli/command/stack/loader"
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/pkg/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
)
|
||||
|
||||
// RunDeploy is the kubernetes implementation of docker stack deploy
|
||||
@ -21,16 +16,6 @@ func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error {
|
||||
return errors.Errorf("Please specify only one compose file (with --compose-file).")
|
||||
}
|
||||
|
||||
// Parse the compose file
|
||||
cfg, err := loader.LoadComposefile(dockerCli, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stack, err := LoadStack(opts.Namespace, *cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Initialize clients
|
||||
stacks, err := dockerCli.stacks()
|
||||
if err != nil {
|
||||
@ -40,6 +25,17 @@ func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse the compose file
|
||||
cfg, err := loader.LoadComposefile(dockerCli, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stack, err := stacks.FromCompose(opts.Namespace, *cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configMaps := composeClient.ConfigMaps()
|
||||
secrets := composeClient.Secrets()
|
||||
services := composeClient.Services()
|
||||
@ -48,93 +44,27 @@ func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error {
|
||||
Pods: pods,
|
||||
}
|
||||
|
||||
// FIXME(vdemeester) handle warnings server-side
|
||||
if err = IsColliding(services, stack, cfg); err != nil {
|
||||
if err := stacks.IsColliding(services, stack); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = createFileBasedConfigMaps(stack.Name, cfg.Configs, configMaps); err != nil {
|
||||
if err := stack.createFileBasedConfigMaps(configMaps); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = createFileBasedSecrets(stack.Name, cfg.Secrets, secrets); err != nil {
|
||||
if err := stack.createFileBasedSecrets(secrets); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if in, err := stacks.Get(stack.Name, metav1.GetOptions{}); err == nil {
|
||||
in.Spec = stack.Spec
|
||||
|
||||
if _, err = stacks.Update(in); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Stack %s was updated\n", stack.Name)
|
||||
} else {
|
||||
if _, err = stacks.Create(stack); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(cmdOut, "Stack %s was created\n", stack.Name)
|
||||
if err = stacks.CreateOrUpdate(stack); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(cmdOut, "Waiting for the stack to be stable and running...")
|
||||
|
||||
<-watcher.Watch(stack, serviceNames(cfg))
|
||||
<-watcher.Watch(stack.name, stack.getServices())
|
||||
|
||||
fmt.Fprintf(cmdOut, "Stack %s is stable and running\n\n", stack.Name)
|
||||
// TODO: fmt.Fprintf(cmdOut, "Read the logs with:\n $ %s stack logs %s\n", filepath.Base(os.Args[0]), stack.Name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createFileBasedConfigMaps creates a Kubernetes ConfigMap for each Compose global file-based config.
|
||||
func createFileBasedConfigMaps(stackName string, globalConfigs map[string]composetypes.ConfigObjConfig, configMaps corev1.ConfigMapInterface) error {
|
||||
for name, config := range globalConfigs {
|
||||
if config.File == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fileName := path.Base(config.File)
|
||||
content, err := ioutil.ReadFile(config.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := configMaps.Create(toConfigMap(stackName, name, fileName, content)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func serviceNames(cfg *composetypes.Config) []string {
|
||||
names := []string{}
|
||||
|
||||
for _, service := range cfg.Services {
|
||||
names = append(names, service.Name)
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
// createFileBasedSecrets creates a Kubernetes Secret for each Compose global file-based secret.
|
||||
func createFileBasedSecrets(stackName string, globalSecrets map[string]composetypes.SecretConfig, secrets corev1.SecretInterface) error {
|
||||
for name, secret := range globalSecrets {
|
||||
if secret.File == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fileName := path.Base(secret.File)
|
||||
content, err := ioutil.ReadFile(secret.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := secrets.Create(toSecret(stackName, name, fileName, content)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(cmdOut, "Stack %s is stable and running\n\n", stack.name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -5,8 +5,6 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"vbom.ml/util/sortorder"
|
||||
)
|
||||
@ -46,29 +44,11 @@ func getStacks(kubeCli *KubeCli) ([]*formatter.Stack, error) {
|
||||
return nil, err
|
||||
}
|
||||
var formattedStacks []*formatter.Stack
|
||||
for _, stack := range stacks.Items {
|
||||
cfg, err := loadStack(stack.Spec.ComposeFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, stack := range stacks {
|
||||
formattedStacks = append(formattedStacks, &formatter.Stack{
|
||||
Name: stack.Name,
|
||||
Services: len(getServices(cfg)),
|
||||
Name: stack.name,
|
||||
Services: len(stack.getServices()),
|
||||
})
|
||||
}
|
||||
return formattedStacks, nil
|
||||
}
|
||||
|
||||
func loadStack(composefile string) (*composetypes.Config, error) {
|
||||
parsed, err := loader.ParseYAML([]byte(composefile))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return loader.Load(composetypes.ConfigDetails{
|
||||
ConfigFiles: []composetypes.ConfigFile{
|
||||
{
|
||||
Config: parsed,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// LoadStack loads a stack from a Compose config, with a given name.
|
||||
func LoadStack(name string, cfg composetypes.Config) (*apiv1beta1.Stack, error) {
|
||||
res, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &apiv1beta1.Stack{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Spec: apiv1beta1.StackSpec{
|
||||
ComposeFile: string(res),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@ -1,55 +0,0 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/gotestyourself/gotestyourself/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestLoadStack(t *testing.T) {
|
||||
s, err := LoadStack("foo", composetypes.Config{
|
||||
Version: "3.1",
|
||||
Filename: "banana",
|
||||
Services: []composetypes.ServiceConfig{
|
||||
{
|
||||
Name: "foo",
|
||||
Image: "foo",
|
||||
},
|
||||
{
|
||||
Name: "bar",
|
||||
Image: "bar",
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, &apiv1beta1.Stack{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Spec: apiv1beta1.StackSpec{
|
||||
ComposeFile: `version: "3.1"
|
||||
services:
|
||||
bar:
|
||||
image: bar
|
||||
foo:
|
||||
image: foo
|
||||
networks: {}
|
||||
volumes: {}
|
||||
secrets: {}
|
||||
configs: {}
|
||||
`,
|
||||
},
|
||||
}, s, cmpKubeAPITime)
|
||||
}
|
||||
|
||||
// TODO: this can be removed when k8s.io/apimachinery is updated to > 1.9.0
|
||||
var cmpKubeAPITime = cmp.Comparer(func(x, y *metav1.Time) bool {
|
||||
if x == nil || y == nil {
|
||||
return x == y
|
||||
}
|
||||
return x.Time.Equal(y.Time)
|
||||
})
|
||||
@ -32,7 +32,7 @@ func RunPS(dockerCli *KubeCli, options options.PS) error {
|
||||
podsClient := client.Pods()
|
||||
|
||||
// Fetch pods
|
||||
if _, err := stacks.Get(namespace, metav1.GetOptions{}); err != nil {
|
||||
if _, err := stacks.Get(namespace); err != nil {
|
||||
return fmt.Errorf("nothing found in stack: %s", namespace)
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// RunRemove is the kubernetes implementation of docker stack remove
|
||||
@ -15,7 +14,7 @@ func RunRemove(dockerCli *KubeCli, opts options.Remove) error {
|
||||
}
|
||||
for _, stack := range opts.Namespaces {
|
||||
fmt.Fprintf(dockerCli.Out(), "Removing stack: %s\n", stack)
|
||||
err := stacks.Delete(stack, &metav1.DeleteOptions{})
|
||||
err := stacks.Delete(stack)
|
||||
if err != nil {
|
||||
fmt.Fprintf(dockerCli.Out(), "Failed to remove stack %s: %s\n", stack, err)
|
||||
return err
|
||||
|
||||
@ -22,7 +22,7 @@ func RunServices(dockerCli *KubeCli, opts options.Services) error {
|
||||
}
|
||||
replicas := client.ReplicaSets()
|
||||
|
||||
if _, err := stacks.Get(opts.Namespace, metav1.GetOptions{}); err != nil {
|
||||
if _, err := stacks.Get(opts.Namespace); err != nil {
|
||||
fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", opts.Namespace)
|
||||
return nil
|
||||
}
|
||||
|
||||
106
components/cli/cli/command/stack/kubernetes/stack.go
Normal file
106
components/cli/cli/command/stack/kubernetes/stack.go
Normal file
@ -0,0 +1,106 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"sort"
|
||||
|
||||
"github.com/docker/cli/kubernetes/compose/v1beta2"
|
||||
"github.com/docker/cli/kubernetes/labels"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/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.
|
||||
type stack struct {
|
||||
name 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 {
|
||||
services[i] = service.Name
|
||||
}
|
||||
sort.Strings(services)
|
||||
return services
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if config.File == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fileName := path.Base(config.File)
|
||||
content, err := ioutil.ReadFile(config.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := configMaps.Create(toConfigMap(s.name, name, fileName, content)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// toConfigMap converts a Compose Config to a Kube ConfigMap.
|
||||
func toConfigMap(stackName, name, key string, content []byte) *apiv1.ConfigMap {
|
||||
return &apiv1.ConfigMap{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Labels: map[string]string{
|
||||
labels.ForStackName: stackName,
|
||||
},
|
||||
},
|
||||
Data: map[string]string{
|
||||
key: string(content),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if secret.File == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fileName := path.Base(secret.File)
|
||||
content, err := ioutil.ReadFile(secret.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := secrets.Create(toSecret(s.name, name, fileName, content)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// toSecret converts a Compose Secret to a Kube Secret.
|
||||
func toSecret(stackName, name, key string, content []byte) *apiv1.Secret {
|
||||
return &apiv1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Labels: map[string]string{
|
||||
labels.ForStackName: stackName,
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
key: content,
|
||||
},
|
||||
}
|
||||
}
|
||||
177
components/cli/cli/command/stack/kubernetes/stackclient.go
Normal file
177
components/cli/cli/command/stack/kubernetes/stackclient.go
Normal file
@ -0,0 +1,177 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
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"
|
||||
"github.com/docker/cli/kubernetes/labels"
|
||||
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"
|
||||
)
|
||||
|
||||
// stackClient talks to a kubernetes compose component.
|
||||
type stackClient interface {
|
||||
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(name string, cfg composetypes.Config) (stack, error)
|
||||
}
|
||||
|
||||
// stackV1Beta1 implements stackClient interface and talks to compose component v1beta1.
|
||||
type stackV1Beta1 struct {
|
||||
stacks composev1beta1.StackInterface
|
||||
}
|
||||
|
||||
func newStackV1Beta1(config *rest.Config, namespace string) (stackClient, error) {
|
||||
client, err := composev1beta1.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &stackV1Beta1{stacks: client.Stacks(namespace)}, nil
|
||||
}
|
||||
|
||||
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
|
||||
_, err := s.stacks.Update(stackBeta1)
|
||||
return err
|
||||
}
|
||||
// Or create it
|
||||
_, err := s.stacks.Create(stackToV1beta1(internalStack))
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *stackV1Beta1) Delete(name string) error {
|
||||
return s.stacks.Delete(name, &metav1.DeleteOptions{})
|
||||
}
|
||||
|
||||
func (s *stackV1Beta1) Get(name string) (stack, error) {
|
||||
stackBeta1, err := s.stacks.Get(name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return stack{}, err
|
||||
}
|
||||
return stackFromV1beta1(stackBeta1)
|
||||
}
|
||||
|
||||
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))
|
||||
for i := range list.Items {
|
||||
stack, err := stackFromV1beta1(&list.Items[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stacks[i] = stack
|
||||
}
|
||||
return stacks, nil
|
||||
}
|
||||
|
||||
// IsColliding verifies that services defined in the stack collides with already deployed services
|
||||
func (s *stackV1Beta1) IsColliding(servicesClient corev1.ServiceInterface, st stack) error {
|
||||
for _, srv := range st.getServices() {
|
||||
if err := verify(servicesClient, st.name, srv); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// verify checks wether the service is already present in kubernetes.
|
||||
// If we find the service by name but it doesn't have our label or it has a different value
|
||||
// than the stack name for the label, we fail (i.e. it will collide)
|
||||
func verify(services corev1.ServiceInterface, stackName string, service string) error {
|
||||
svc, err := services.Get(service, metav1.GetOptions{})
|
||||
if err == nil {
|
||||
if key, ok := svc.ObjectMeta.Labels[labels.ForStackName]; ok {
|
||||
if key != stackName {
|
||||
return fmt.Errorf("service %s already present in stack named %s", service, key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("service %s already present in the cluster", service)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stackV1Beta1) FromCompose(name string, cfg composetypes.Config) (stack, error) {
|
||||
res, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return stack{}, err
|
||||
}
|
||||
return stack{
|
||||
name: name,
|
||||
composeFile: string(res),
|
||||
spec: fromComposeConfig(&cfg),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// stackV1Beta2 implements stackClient interface and talks to compose component v1beta2.
|
||||
type stackV1Beta2 struct {
|
||||
stacks composev1beta2.StackInterface
|
||||
}
|
||||
|
||||
func newStackV1Beta2(config *rest.Config, namespace string) (stackClient, error) {
|
||||
client, err := composev1beta2.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &stackV1Beta2{stacks: client.Stacks(namespace)}, nil
|
||||
}
|
||||
|
||||
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
|
||||
_, err := s.stacks.Update(stackBeta2)
|
||||
return err
|
||||
}
|
||||
// Or create it
|
||||
_, err := s.stacks.Create(stackToV1beta2(internalStack))
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *stackV1Beta2) Delete(name string) error {
|
||||
return s.stacks.Delete(name, &metav1.DeleteOptions{})
|
||||
}
|
||||
|
||||
func (s *stackV1Beta2) Get(name string) (stack, error) {
|
||||
stackBeta2, err := s.stacks.Get(name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return stack{}, err
|
||||
}
|
||||
return stackFromV1beta2(stackBeta2), nil
|
||||
}
|
||||
|
||||
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))
|
||||
for i := range list.Items {
|
||||
stacks[i] = stackFromV1beta2(&list.Items[i])
|
||||
}
|
||||
return stacks, nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stackV1Beta2) FromCompose(name string, cfg composetypes.Config) (stack, error) {
|
||||
return stack{
|
||||
name: name,
|
||||
spec: fromComposeConfig(&cfg),
|
||||
}, nil
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/gotestyourself/gotestyourself/assert"
|
||||
)
|
||||
|
||||
func TestFromCompose(t *testing.T) {
|
||||
stackClient := &stackV1Beta1{}
|
||||
s, err := stackClient.FromCompose("foo", composetypes.Config{
|
||||
Version: "3.1",
|
||||
Filename: "banana",
|
||||
Services: []composetypes.ServiceConfig{
|
||||
{
|
||||
Name: "foo",
|
||||
Image: "foo",
|
||||
},
|
||||
{
|
||||
Name: "bar",
|
||||
Image: "bar",
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, "foo", s.name)
|
||||
assert.Equal(t, string(`version: "3.1"
|
||||
services:
|
||||
bar:
|
||||
image: bar
|
||||
foo:
|
||||
image: foo
|
||||
networks: {}
|
||||
volumes: {}
|
||||
secrets: {}
|
||||
configs: {}
|
||||
`), s.composeFile)
|
||||
}
|
||||
@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1"
|
||||
"github.com/docker/cli/kubernetes/labels"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@ -17,10 +16,10 @@ type DeployWatcher struct {
|
||||
}
|
||||
|
||||
// Watch watches a stuck deployement and return a chan that will holds the state of the stack
|
||||
func (w DeployWatcher) Watch(stack *apiv1beta1.Stack, serviceNames []string) chan bool {
|
||||
func (w DeployWatcher) Watch(name string, serviceNames []string) chan bool {
|
||||
stop := make(chan bool)
|
||||
|
||||
go w.waitForPods(stack.Name, serviceNames, stop)
|
||||
go w.waitForPods(name, serviceNames, stop)
|
||||
|
||||
return stop
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user