1
0
mirror of https://github.com/docker/cli.git synced 2026-01-26 15:41:42 +03:00

Add support for kubernetes in docker cli

- Add support for kubernetes for docker stack command
- Update to go 1.9
- Add kubernetes to vendors
- Print orchestrator in docker version command

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
Signed-off-by: Silvin Lubecki <silvin.lubecki@docker.com>
This commit is contained in:
Vincent Demeester
2017-11-20 15:30:52 +01:00
committed by Silvin Lubecki
parent 70db7cc0fc
commit 8417e49792
985 changed files with 319793 additions and 3422 deletions

View File

@@ -3,6 +3,9 @@ package stack
import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/orchestrator"
"github.com/docker/cli/cli/command/stack/kubernetes"
"github.com/docker/cli/cli/command/stack/swarm"
"github.com/spf13/cobra"
)
@@ -15,19 +18,24 @@ func NewStackCommand(dockerCli command.Cli) *cobra.Command {
RunE: command.ShowHelp(dockerCli.Err()),
Annotations: map[string]string{"version": "1.25"},
}
cmd.AddCommand(
newDeployCommand(dockerCli),
newListCommand(dockerCli),
newRemoveCommand(dockerCli),
newServicesCommand(dockerCli),
newPsCommand(dockerCli),
)
switch orchestrator.GetOrchestrator(dockerCli) {
case orchestrator.Kubernetes:
kubernetes.AddStackCommands(cmd, dockerCli)
case orchestrator.Swarm:
swarm.AddStackCommands(cmd, dockerCli)
}
return cmd
}
// NewTopLevelDeployCommand returns a command for `docker deploy`
func NewTopLevelDeployCommand(dockerCli command.Cli) *cobra.Command {
cmd := newDeployCommand(dockerCli)
var cmd *cobra.Command
switch orchestrator.GetOrchestrator(dockerCli) {
case orchestrator.Kubernetes:
cmd = kubernetes.NewTopLevelDeployCommand(dockerCli)
case orchestrator.Swarm:
cmd = swarm.NewTopLevelDeployCommand(dockerCli)
}
// Remove the aliases at the top level
cmd.Aliases = []string{}
cmd.Annotations = map[string]string{"experimental": "", "version": "1.25"}

View File

@@ -1,4 +1,4 @@
package stack
package common
import (
"fmt"
@@ -10,21 +10,25 @@ import (
"github.com/spf13/pflag"
)
func addComposefileFlag(opt *string, flags *pflag.FlagSet) {
// AddComposefileFlag adds compose-file file to the specified flagset
func AddComposefileFlag(opt *string, flags *pflag.FlagSet) {
flags.StringVarP(opt, "compose-file", "c", "", "Path to a Compose file")
flags.SetAnnotation("compose-file", "version", []string{"1.25"})
}
func addBundlefileFlag(opt *string, flags *pflag.FlagSet) {
// AddBundlefileFlag adds bundle-file file to the specified flagset
func AddBundlefileFlag(opt *string, flags *pflag.FlagSet) {
flags.StringVar(opt, "bundle-file", "", "Path to a Distributed Application Bundle file")
flags.SetAnnotation("bundle-file", "experimental", nil)
}
func addRegistryAuthFlag(opt *bool, flags *pflag.FlagSet) {
// AddRegistryAuthFlag adds with-registry-auth file to the specified flagset
func AddRegistryAuthFlag(opt *bool, flags *pflag.FlagSet) {
flags.BoolVar(opt, "with-registry-auth", false, "Send registry authentication details to Swarm agents")
}
func loadBundlefile(stderr io.Writer, namespace string, path string) (*bundlefile.Bundlefile, error) {
// LoadBundlefile loads a bundle-file from the specified path
func LoadBundlefile(stderr io.Writer, namespace string, path string) (*bundlefile.Bundlefile, error) {
defaultPath := fmt.Sprintf("%s.dab", namespace)
if path == "" {

View File

@@ -1,4 +1,4 @@
package stack
package common
import (
"bytes"
@@ -32,7 +32,7 @@ func TestLoadBundlefileErrors(t *testing.T) {
}
for _, tc := range testCases {
_, err := loadBundlefile(&bytes.Buffer{}, tc.namespace, tc.path)
_, err := LoadBundlefile(&bytes.Buffer{}, tc.namespace, tc.path)
assert.Error(t, err, tc.expectedError)
}
}
@@ -42,7 +42,7 @@ func TestLoadBundlefile(t *testing.T) {
namespace := ""
path := filepath.Join("testdata", "bundlefile_with_two_services.dab")
bundleFile, err := loadBundlefile(buf, namespace, path)
bundleFile, err := LoadBundlefile(buf, namespace, path)
assert.NoError(t, err)
assert.Equal(t, len(bundleFile.Services), 2)

View File

@@ -0,0 +1,88 @@
package clientset
import (
composev1beta1 "github.com/docker/cli/cli/command/stack/kubernetes/api/client/clientset_generated/clientset/typed/compose/v1beta1"
glog "github.com/golang/glog"
discovery "k8s.io/client-go/discovery"
rest "k8s.io/client-go/rest"
flowcontrol "k8s.io/client-go/util/flowcontrol"
)
type Interface interface {
Discovery() discovery.DiscoveryInterface
ComposeV1beta1() composev1beta1.ComposeV1beta1Interface
// Deprecated: please explicitly pick a version if possible.
Compose() composev1beta1.ComposeV1beta1Interface
}
// Clientset contains the clients for groups. Each group has exactly one
// version included in a Clientset.
type Clientset struct {
*discovery.DiscoveryClient
*composev1beta1.ComposeV1beta1Client
}
// ComposeV1beta1 retrieves the ComposeV1beta1Client
func (c *Clientset) ComposeV1beta1() composev1beta1.ComposeV1beta1Interface {
if c == nil {
return nil
}
return c.ComposeV1beta1Client
}
// Deprecated: Compose retrieves the default version of ComposeClient.
// Please explicitly pick a version.
func (c *Clientset) Compose() composev1beta1.ComposeV1beta1Interface {
if c == nil {
return nil
}
return c.ComposeV1beta1Client
}
// Discovery retrieves the DiscoveryClient
func (c *Clientset) Discovery() discovery.DiscoveryInterface {
if c == nil {
return nil
}
return c.DiscoveryClient
}
// NewForConfig creates a new Clientset for the given config.
func NewForConfig(c *rest.Config) (*Clientset, error) {
configShallowCopy := *c
if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 {
configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst)
}
var cs Clientset
var err error
cs.ComposeV1beta1Client, err = composev1beta1.NewForConfig(&configShallowCopy)
if err != nil {
return nil, err
}
cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy)
if err != nil {
glog.Errorf("failed to create the DiscoveryClient: %v", err)
return nil, err
}
return &cs, nil
}
// NewForConfigOrDie creates a new Clientset for the given config and
// panics if there is an error in the config.
func NewForConfigOrDie(c *rest.Config) *Clientset {
var cs Clientset
cs.ComposeV1beta1Client = composev1beta1.NewForConfigOrDie(c)
cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c)
return &cs
}
// New creates a new Clientset for the given RESTClient.
func New(c rest.Interface) *Clientset {
var cs Clientset
cs.ComposeV1beta1Client = composev1beta1.New(c)
cs.DiscoveryClient = discovery.NewDiscoveryClient(c)
return &cs
}

View File

@@ -0,0 +1,4 @@
// This package is generated by client-gen with custom arguments.
// This package has the automatically generated clientset.
package clientset

View File

@@ -0,0 +1,4 @@
// This package is generated by client-gen with custom arguments.
// This package contains the scheme of the automatically generated clientset.
package scheme

View File

@@ -0,0 +1,37 @@
package scheme
import (
composev1beta1 "github.com/docker/cli/cli/command/stack/kubernetes/api/compose/v1beta1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
serializer "k8s.io/apimachinery/pkg/runtime/serializer"
)
var Scheme = runtime.NewScheme()
var Codecs = serializer.NewCodecFactory(Scheme)
var ParameterCodec = runtime.NewParameterCodec(Scheme)
func init() {
v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"})
AddToScheme(Scheme)
}
// AddToScheme adds all types of this clientset into the given scheme. This allows composition
// of clientsets, like in:
//
// import (
// "k8s.io/client-go/kubernetes"
// clientsetscheme "k8s.io/client-go/kuberentes/scheme"
// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
// )
//
// kclientset, _ := kubernetes.NewForConfig(c)
// aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
//
// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
// correctly.
func AddToScheme(scheme *runtime.Scheme) {
composev1beta1.AddToScheme(scheme)
}

View File

@@ -0,0 +1,72 @@
package v1beta1
import (
"github.com/docker/cli/cli/command/stack/kubernetes/api/client/clientset_generated/clientset/scheme"
v1beta1 "github.com/docker/cli/cli/command/stack/kubernetes/api/compose/v1beta1"
serializer "k8s.io/apimachinery/pkg/runtime/serializer"
rest "k8s.io/client-go/rest"
)
type ComposeV1beta1Interface interface {
RESTClient() rest.Interface
StacksGetter
}
// ComposeV1beta1Client is used to interact with features provided by the compose.docker.com group.
type ComposeV1beta1Client struct {
restClient rest.Interface
}
func (c *ComposeV1beta1Client) Stacks(namespace string) StackInterface {
return newStacks(c, namespace)
}
// NewForConfig creates a new ComposeV1beta1Client for the given config.
func NewForConfig(c *rest.Config) (*ComposeV1beta1Client, error) {
config := *c
if err := setConfigDefaults(&config); err != nil {
return nil, err
}
client, err := rest.RESTClientFor(&config)
if err != nil {
return nil, err
}
return &ComposeV1beta1Client{client}, nil
}
// NewForConfigOrDie creates a new ComposeV1beta1Client for the given config and
// panics if there is an error in the config.
func NewForConfigOrDie(c *rest.Config) *ComposeV1beta1Client {
client, err := NewForConfig(c)
if err != nil {
panic(err)
}
return client
}
// New creates a new ComposeV1beta1Client for the given RESTClient.
func New(c rest.Interface) *ComposeV1beta1Client {
return &ComposeV1beta1Client{c}
}
func setConfigDefaults(config *rest.Config) error {
gv := v1beta1.SchemeGroupVersion
config.GroupVersion = &gv
config.APIPath = "/apis"
config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: scheme.Codecs}
if config.UserAgent == "" {
config.UserAgent = rest.DefaultKubernetesUserAgent()
}
return nil
}
// RESTClient returns a RESTClient that is used to communicate
// with API server by this client implementation.
func (c *ComposeV1beta1Client) RESTClient() rest.Interface {
if c == nil {
return nil
}
return c.restClient
}

View File

@@ -0,0 +1,4 @@
// This package is generated by client-gen with custom arguments.
// This package has the automatically generated typed clients.
package v1beta1

View File

@@ -0,0 +1,3 @@
package v1beta1
type StackExpansion interface{}

View File

@@ -0,0 +1,158 @@
package v1beta1
import (
scheme "github.com/docker/cli/cli/command/stack/kubernetes/api/client/clientset_generated/clientset/scheme"
v1beta1 "github.com/docker/cli/cli/command/stack/kubernetes/api/compose/v1beta1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
rest "k8s.io/client-go/rest"
)
// StacksGetter has a method to return a StackInterface.
// A group's client should implement this interface.
type StacksGetter interface {
Stacks(namespace string) StackInterface
}
// StackInterface has methods to work with Stack resources.
type StackInterface interface {
Create(*v1beta1.Stack) (*v1beta1.Stack, error)
Update(*v1beta1.Stack) (*v1beta1.Stack, error)
UpdateStatus(*v1beta1.Stack) (*v1beta1.Stack, error)
Delete(name string, options *v1.DeleteOptions) error
DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error
Get(name string, options v1.GetOptions) (*v1beta1.Stack, error)
List(opts v1.ListOptions) (*v1beta1.StackList, error)
Watch(opts v1.ListOptions) (watch.Interface, error)
Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1beta1.Stack, err error)
StackExpansion
}
var _ StackInterface = &stacks{}
// stacks implements StackInterface
type stacks struct {
client rest.Interface
ns string
}
// newStacks returns a Stacks
func newStacks(c *ComposeV1beta1Client, namespace string) *stacks {
return &stacks{
client: c.RESTClient(),
ns: namespace,
}
}
// Create takes the representation of a stack and creates it. Returns the server's representation of the stack, and an error, if there is any.
func (c *stacks) Create(stack *v1beta1.Stack) (result *v1beta1.Stack, err error) {
result = &v1beta1.Stack{}
err = c.client.Post().
Namespace(c.ns).
Resource("stacks").
Body(stack).
Do().
Into(result)
return
}
// Update takes the representation of a stack and updates it. Returns the server's representation of the stack, and an error, if there is any.
func (c *stacks) Update(stack *v1beta1.Stack) (result *v1beta1.Stack, err error) {
result = &v1beta1.Stack{}
err = c.client.Put().
Namespace(c.ns).
Resource("stacks").
Name(stack.Name).
Body(stack).
Do().
Into(result)
return
}
// UpdateStatus was generated because the type contains a Status member.
// Add a +genclientstatus=false comment above the type to avoid generating UpdateStatus().
func (c *stacks) UpdateStatus(stack *v1beta1.Stack) (result *v1beta1.Stack, err error) {
result = &v1beta1.Stack{}
err = c.client.Put().
Namespace(c.ns).
Resource("stacks").
Name(stack.Name).
SubResource("status").
Body(stack).
Do().
Into(result)
return
}
// Delete takes name of the stack and deletes it. Returns an error if one occurs.
func (c *stacks) Delete(name string, options *v1.DeleteOptions) error {
return c.client.Delete().
Namespace(c.ns).
Resource("stacks").
Name(name).
Body(options).
Do().
Error()
}
// DeleteCollection deletes a collection of objects.
func (c *stacks) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
return c.client.Delete().
Namespace(c.ns).
Resource("stacks").
VersionedParams(&listOptions, scheme.ParameterCodec).
Body(options).
Do().
Error()
}
// Get takes name of the stack, and returns the corresponding stack object, and an error if there is any.
func (c *stacks) Get(name string, options v1.GetOptions) (result *v1beta1.Stack, err error) {
result = &v1beta1.Stack{}
err = c.client.Get().
Namespace(c.ns).
Resource("stacks").
Name(name).
VersionedParams(&options, scheme.ParameterCodec).
Do().
Into(result)
return
}
// List takes label and field selectors, and returns the list of Stacks that match those selectors.
func (c *stacks) List(opts v1.ListOptions) (result *v1beta1.StackList, err error) {
result = &v1beta1.StackList{}
err = c.client.Get().
Namespace(c.ns).
Resource("stacks").
VersionedParams(&opts, scheme.ParameterCodec).
Do().
Into(result)
return
}
// Watch returns a watch.Interface that watches the requested stacks.
func (c *stacks) Watch(opts v1.ListOptions) (watch.Interface, error) {
opts.Watch = true
return c.client.Get().
Namespace(c.ns).
Resource("stacks").
VersionedParams(&opts, scheme.ParameterCodec).
Watch()
}
// Patch applies the patch and returns the patched stack.
func (c *stacks) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1beta1.Stack, err error) {
result = &v1beta1.Stack{}
err = c.client.Patch(pt).
Namespace(c.ns).
Resource("stacks").
SubResource(subresources...).
Name(name).
Body(data).
Do().
Into(result)
return
}

View File

@@ -0,0 +1,5 @@
// +k8s:deepcopy-gen=package,register
// +groupName=compose.docker.com
// Package compose is the internal version of the API.
package compose

View File

@@ -0,0 +1,43 @@
package compose
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// GroupName is the group name used to register these objects
const GroupName = "compose.docker.com"
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}
// Kind takes an unqualified kind and returns back a Group qualified GroupKind
func Kind(kind string) schema.GroupKind {
return SchemeGroupVersion.WithKind(kind).GroupKind()
}
// Resource takes an unqualified resource and returns back a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}
var (
// SchemeBuilder collects functions that add things to a scheme. It's to allow
// code to compile without explicitly referencing generated types. You should
// declare one in each package that will have generated deep copy or conversion
// functions.
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
// AddToScheme applies all the stored functions to the scheme. A non-nil error
// indicates that one function failed and the attempt was abandoned.
AddToScheme = SchemeBuilder.AddToScheme
)
// adds the list of known types to api.Scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Stack{},
&StackList{},
)
return nil
}

View File

@@ -0,0 +1,117 @@
package compose
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// ImpersonationConfig holds information use to impersonate calls from the compose controller
type ImpersonationConfig struct {
// UserName is the username to impersonate on each request.
UserName string
// Groups are the groups to impersonate on each request.
Groups []string
// Extra is a free-form field which can be used to link some authentication information
// to authorization information. This field allows you to impersonate it.
Extra map[string][]string
}
// Stack defines a stack object to be register in the kubernetes API
// +genclient=true
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type Stack struct {
metav1.TypeMeta
metav1.ObjectMeta
Spec StackSpec
Status StackStatus
}
// StackStatus defines the observed state of Stack
type StackStatus struct {
Phase StackPhase
Message string
}
// StackSpec defines the desired state of Stack
type StackSpec struct {
ComposeFile string
Owner ImpersonationConfig
}
// StackPhase defines the status phase in which the stack is.
type StackPhase string
// These are valid conditions of a stack.
const (
// Available means the stack is available.
StackAvailable StackPhase = "Available"
// Progressing means the deployment is progressing.
StackProgressing StackPhase = "Progressing"
// StackFailure is added in a stack when one of its members fails to be created
// or deleted.
StackFailure StackPhase = "Failure"
)
// StackList defines a list of stacks
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type StackList struct {
metav1.TypeMeta
metav1.ListMeta
Items []Stack
}
// Owner defines the owner of a stack. It is used to impersonate the controller calls
// to kubernetes api.
type Owner struct {
metav1.TypeMeta
metav1.ObjectMeta
Owner ImpersonationConfig
}
// OwnerList defines a list of owner.
type OwnerList struct {
metav1.TypeMeta
metav1.ListMeta
Items []Owner
}
// FIXME(vdemeester) are those necessary ??
// NewStatus is newStatus
func (Stack) NewStatus() interface{} {
return StackStatus{}
}
// GetStatus returns the status
func (pc *Stack) GetStatus() interface{} {
return pc.Status
}
// SetStatus sets the status
func (pc *Stack) SetStatus(s interface{}) {
pc.Status = s.(StackStatus)
}
// GetSpec returns the spec
func (pc *Stack) GetSpec() interface{} {
return pc.Spec
}
// SetSpec sets the spec
func (pc *Stack) SetSpec(s interface{}) {
pc.Spec = s.(StackSpec)
}
// GetObjectMeta returns the ObjectMeta
func (pc *Stack) GetObjectMeta() *metav1.ObjectMeta {
return &pc.ObjectMeta
}
// SetGeneration sets the Generation
func (pc *Stack) SetGeneration(generation int64) {
pc.ObjectMeta.Generation = generation
}
// GetGeneration returns the Generation
func (pc Stack) GetGeneration() int64 {
return pc.ObjectMeta.Generation
}

View File

@@ -0,0 +1,11 @@
// Package v1beta1 holds the v1beta1 versions of our stack structures.
// API versions allow the api contract for a resource to be changed while keeping
// backward compatibility by support multiple concurrent versions
// of the same resource
//
// +k8s:openapi-gen=true
// +k8s:deepcopy-gen=package,register
// +k8s:conversion-gen=github.com/docker/cli/cli/command/stack/kubernetes/api/compose
// +k8s:defaulter-gen=TypeMeta
// +groupName=compose.docker.com
package v1beta1 // import "github.com/docker/cli/cli/command/stack/kubernetes/api/compose/v1beta1"

View File

@@ -0,0 +1,25 @@
package v1beta1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/docker/cli/cli/command/stack/kubernetes/api/compose"
)
// Owner defines the owner of a stack. It is used to impersonate the controller calls
// to kubernetes api.
// +genclient=true
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +subresource-request
type Owner struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Owner compose.ImpersonationConfig `json:"owner,omitempty"`
}
// OwnerList defines a list of owner.
type OwnerList struct {
metav1.TypeMeta
metav1.ListMeta
Items []Owner
}

View File

@@ -0,0 +1,47 @@
package v1beta1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// GroupName is the group name used to register these objects
const GroupName = "compose.docker.com"
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1beta1"}
var (
// SchemeBuilder collects functions that add things to a scheme. It's to allow
// code to compile without explicitly referencing generated types. You should
// declare one in each package that will have generated deep copy or conversion
// functions.
SchemeBuilder runtime.SchemeBuilder
localSchemeBuilder = &SchemeBuilder
// AddToScheme applies all the stored functions to the scheme. A non-nil error
// indicates that one function failed and the attempt was abandoned.
AddToScheme = localSchemeBuilder.AddToScheme
)
func init() {
localSchemeBuilder.Register(addKnownTypes)
}
// Adds the list of known types to api.Scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Stack{},
&StackList{},
&Owner{},
&OwnerList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}
// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}

View File

@@ -0,0 +1,68 @@
package v1beta1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// StackList defines a list of stacks
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type StackList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
Items []Stack `json:"items" protobuf:"bytes,2,rep,name=items"`
}
// +genclient=true
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// Stack defines a stack object to be register in the kubernetes API
// +k8s:openapi-gen=true
// +resource:path=stacks,strategy=StackStrategy
// +subresource:request=Owner,path=owner,rest=OwnerStackREST
type Stack struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec StackSpec `json:"spec,omitempty"`
Status StackStatus `json:"status,omitempty"`
}
// StackSpec defines the desired state of Stack
type StackSpec struct {
ComposeFile string `json:"composeFile,omitempty"`
}
// StackPhase defines the status phase in which the stack is.
type StackPhase string
// These are valid conditions of a stack.
const (
// Available means the stack is available.
StackAvailable StackPhase = "Available"
// Progressing means the deployment is progressing.
StackProgressing StackPhase = "Progressing"
// StackFailure is added in a stack when one of its members fails to be created
// or deleted.
StackFailure StackPhase = "Failure"
)
// StackStatus defines the observed state of Stack
type StackStatus struct {
// Current condition of the stack.
// +optional
Phase StackPhase `json:"phase,omitempty" protobuf:"bytes,1,opt,name=phase,casttype=StackPhase"`
// A human readable message indicating details about the stack.
// +optional
Message string `json:"message,omitempty" protobuf:"bytes,5,opt,name=message"`
}
// Clone implements the Cloner interface for kubernetes
func (s *Stack) Clone() (*Stack, error) {
scheme := runtime.NewScheme()
if err := AddToScheme(scheme); err != nil {
return nil, err
}
return s.DeepCopy(), nil
}

View File

@@ -0,0 +1,9 @@
version: "3.2"
services:
nginx:
image: nginx
deploy:
replicas: 2
redis:
image: redis:alpine

View File

@@ -0,0 +1,10 @@
version: "3.2"
services:
nginx:
image: nginx
deploy:
replicas: 2
redis:
image: redis:alpine
deploy:
replicas: 5

View File

@@ -0,0 +1,10 @@
version: "3.2"
services:
redis:
image: redis:alpine
deploy:
resources:
limits:
memory: 64M
reservations:
memory: 64M

View File

@@ -0,0 +1,11 @@
version: "3.2"
services:
redis:
image: redis:alpine
deploy:
resources:
limits:
memory: 64M
reservations:
memory: 64M
replicas: 5

View File

@@ -0,0 +1,6 @@
version: "3.2"
services:
redis:
image: redis:alpine
deploy:
replicas: 2

View File

@@ -0,0 +1,6 @@
version: "3.2"
services:
redis:
image: redis:alpine
deploy:
replicas: 5

View File

@@ -0,0 +1,4 @@
version: "3.2"
services:
redis:
image: redis:alpine

View File

@@ -0,0 +1,6 @@
version: "3.2"
services:
redis:
image: redis:alpine
deploy:
replicas: 5

View File

@@ -0,0 +1,169 @@
// +build !ignore_autogenerated
// This file was autogenerated by conversion-gen. Do not edit it manually!
package v1beta1
import (
compose "github.com/docker/cli/cli/command/stack/kubernetes/api/compose"
conversion "k8s.io/apimachinery/pkg/conversion"
runtime "k8s.io/apimachinery/pkg/runtime"
)
func init() {
localSchemeBuilder.Register(RegisterConversions)
}
// RegisterConversions adds conversion functions to the given scheme.
// Public to allow building arbitrary schemes.
func RegisterConversions(scheme *runtime.Scheme) error {
return scheme.AddGeneratedConversionFuncs(
Convert_v1beta1_Owner_To_compose_Owner,
Convert_compose_Owner_To_v1beta1_Owner,
Convert_v1beta1_Stack_To_compose_Stack,
Convert_compose_Stack_To_v1beta1_Stack,
Convert_v1beta1_StackList_To_compose_StackList,
Convert_compose_StackList_To_v1beta1_StackList,
Convert_v1beta1_StackSpec_To_compose_StackSpec,
Convert_compose_StackSpec_To_v1beta1_StackSpec,
Convert_v1beta1_StackStatus_To_compose_StackStatus,
Convert_compose_StackStatus_To_v1beta1_StackStatus,
)
}
func autoConvert_v1beta1_Owner_To_compose_Owner(in *Owner, out *compose.Owner, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
out.Owner = in.Owner
return nil
}
// Convert_v1beta1_Owner_To_compose_Owner is an autogenerated conversion function.
func Convert_v1beta1_Owner_To_compose_Owner(in *Owner, out *compose.Owner, s conversion.Scope) error {
return autoConvert_v1beta1_Owner_To_compose_Owner(in, out, s)
}
func autoConvert_compose_Owner_To_v1beta1_Owner(in *compose.Owner, out *Owner, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
out.Owner = in.Owner
return nil
}
// Convert_compose_Owner_To_v1beta1_Owner is an autogenerated conversion function.
func Convert_compose_Owner_To_v1beta1_Owner(in *compose.Owner, out *Owner, s conversion.Scope) error {
return autoConvert_compose_Owner_To_v1beta1_Owner(in, out, s)
}
func autoConvert_v1beta1_Stack_To_compose_Stack(in *Stack, out *compose.Stack, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
if err := Convert_v1beta1_StackSpec_To_compose_StackSpec(&in.Spec, &out.Spec, s); err != nil {
return err
}
if err := Convert_v1beta1_StackStatus_To_compose_StackStatus(&in.Status, &out.Status, s); err != nil {
return err
}
return nil
}
// Convert_v1beta1_Stack_To_compose_Stack is an autogenerated conversion function.
func Convert_v1beta1_Stack_To_compose_Stack(in *Stack, out *compose.Stack, s conversion.Scope) error {
return autoConvert_v1beta1_Stack_To_compose_Stack(in, out, s)
}
func autoConvert_compose_Stack_To_v1beta1_Stack(in *compose.Stack, out *Stack, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
if err := Convert_compose_StackSpec_To_v1beta1_StackSpec(&in.Spec, &out.Spec, s); err != nil {
return err
}
if err := Convert_compose_StackStatus_To_v1beta1_StackStatus(&in.Status, &out.Status, s); err != nil {
return err
}
return nil
}
// Convert_compose_Stack_To_v1beta1_Stack is an autogenerated conversion function.
func Convert_compose_Stack_To_v1beta1_Stack(in *compose.Stack, out *Stack, s conversion.Scope) error {
return autoConvert_compose_Stack_To_v1beta1_Stack(in, out, s)
}
func autoConvert_v1beta1_StackList_To_compose_StackList(in *StackList, out *compose.StackList, s conversion.Scope) error {
out.ListMeta = in.ListMeta
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]compose.Stack, len(*in))
for i := range *in {
if err := Convert_v1beta1_Stack_To_compose_Stack(&(*in)[i], &(*out)[i], s); err != nil {
return err
}
}
} else {
out.Items = nil
}
return nil
}
// Convert_v1beta1_StackList_To_compose_StackList is an autogenerated conversion function.
func Convert_v1beta1_StackList_To_compose_StackList(in *StackList, out *compose.StackList, s conversion.Scope) error {
return autoConvert_v1beta1_StackList_To_compose_StackList(in, out, s)
}
func autoConvert_compose_StackList_To_v1beta1_StackList(in *compose.StackList, out *StackList, s conversion.Scope) error {
out.ListMeta = in.ListMeta
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Stack, len(*in))
for i := range *in {
if err := Convert_compose_Stack_To_v1beta1_Stack(&(*in)[i], &(*out)[i], s); err != nil {
return err
}
}
} else {
out.Items = make([]Stack, 0)
}
return nil
}
// Convert_compose_StackList_To_v1beta1_StackList is an autogenerated conversion function.
func Convert_compose_StackList_To_v1beta1_StackList(in *compose.StackList, out *StackList, s conversion.Scope) error {
return autoConvert_compose_StackList_To_v1beta1_StackList(in, out, s)
}
func autoConvert_v1beta1_StackSpec_To_compose_StackSpec(in *StackSpec, out *compose.StackSpec, s conversion.Scope) error {
out.ComposeFile = in.ComposeFile
return nil
}
func Convert_compose_StackSpec_To_v1beta1_StackSpec(in *compose.StackSpec, out *StackSpec, s conversion.Scope) error {
return autoConvert_compose_StackSpec_To_v1beta1_StackSpec(in, out, s)
}
// Convert_v1beta1_StackSpec_To_compose_StackSpec is an autogenerated conversion function.
func Convert_v1beta1_StackSpec_To_compose_StackSpec(in *StackSpec, out *compose.StackSpec, s conversion.Scope) error {
return autoConvert_v1beta1_StackSpec_To_compose_StackSpec(in, out, s)
}
func autoConvert_compose_StackSpec_To_v1beta1_StackSpec(in *compose.StackSpec, out *StackSpec, s conversion.Scope) error {
out.ComposeFile = in.ComposeFile
return nil
}
func autoConvert_v1beta1_StackStatus_To_compose_StackStatus(in *StackStatus, out *compose.StackStatus, s conversion.Scope) error {
out.Phase = compose.StackPhase(in.Phase)
out.Message = in.Message
return nil
}
// Convert_v1beta1_StackStatus_To_compose_StackStatus is an autogenerated conversion function.
func Convert_v1beta1_StackStatus_To_compose_StackStatus(in *StackStatus, out *compose.StackStatus, s conversion.Scope) error {
return autoConvert_v1beta1_StackStatus_To_compose_StackStatus(in, out, s)
}
func autoConvert_compose_StackStatus_To_v1beta1_StackStatus(in *compose.StackStatus, out *StackStatus, s conversion.Scope) error {
out.Phase = StackPhase(in.Phase)
out.Message = in.Message
return nil
}
// Convert_compose_StackStatus_To_v1beta1_StackStatus is an autogenerated conversion function.
func Convert_compose_StackStatus_To_v1beta1_StackStatus(in *compose.StackStatus, out *StackStatus, s conversion.Scope) error {
return autoConvert_compose_StackStatus_To_v1beta1_StackStatus(in, out, s)
}

View File

@@ -0,0 +1,204 @@
// +build !ignore_autogenerated
// This file was autogenerated by deepcopy-gen. Do not edit it manually!
package v1beta1
import (
conversion "k8s.io/apimachinery/pkg/conversion"
runtime "k8s.io/apimachinery/pkg/runtime"
reflect "reflect"
)
// Deprecated: register deep-copy functions.
func init() {
SchemeBuilder.Register(RegisterDeepCopies)
}
// Deprecated: RegisterDeepCopies adds deep-copy functions to the given scheme. Public
// to allow building arbitrary schemes.
func RegisterDeepCopies(scheme *runtime.Scheme) error {
return scheme.AddGeneratedDeepCopyFuncs(
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
in.(*Owner).DeepCopyInto(out.(*Owner))
return nil
}, InType: reflect.TypeOf(&Owner{})},
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
in.(*OwnerList).DeepCopyInto(out.(*OwnerList))
return nil
}, InType: reflect.TypeOf(&OwnerList{})},
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
in.(*Stack).DeepCopyInto(out.(*Stack))
return nil
}, InType: reflect.TypeOf(&Stack{})},
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
in.(*StackList).DeepCopyInto(out.(*StackList))
return nil
}, InType: reflect.TypeOf(&StackList{})},
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
in.(*StackSpec).DeepCopyInto(out.(*StackSpec))
return nil
}, InType: reflect.TypeOf(&StackSpec{})},
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
in.(*StackStatus).DeepCopyInto(out.(*StackStatus))
return nil
}, InType: reflect.TypeOf(&StackStatus{})},
)
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Owner) DeepCopyInto(out *Owner) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Owner.DeepCopyInto(&out.Owner)
return
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new Owner.
func (x *Owner) DeepCopy() *Owner {
if x == nil {
return nil
}
out := new(Owner)
x.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (x *Owner) DeepCopyObject() runtime.Object {
if c := x.DeepCopy(); c != nil {
return c
} else {
return nil
}
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OwnerList) DeepCopyInto(out *OwnerList) {
*out = *in
out.TypeMeta = in.TypeMeta
out.ListMeta = in.ListMeta
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Owner, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new OwnerList.
func (x *OwnerList) DeepCopy() *OwnerList {
if x == nil {
return nil
}
out := new(OwnerList)
x.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (x *OwnerList) DeepCopyObject() runtime.Object {
if c := x.DeepCopy(); c != nil {
return c
} else {
return nil
}
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Stack) DeepCopyInto(out *Stack) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
out.Status = in.Status
return
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new Stack.
func (x *Stack) DeepCopy() *Stack {
if x == nil {
return nil
}
out := new(Stack)
x.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (x *Stack) DeepCopyObject() runtime.Object {
if c := x.DeepCopy(); c != nil {
return c
} else {
return nil
}
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StackList) DeepCopyInto(out *StackList) {
*out = *in
out.TypeMeta = in.TypeMeta
out.ListMeta = in.ListMeta
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Stack, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new StackList.
func (x *StackList) DeepCopy() *StackList {
if x == nil {
return nil
}
out := new(StackList)
x.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (x *StackList) DeepCopyObject() runtime.Object {
if c := x.DeepCopy(); c != nil {
return c
} else {
return nil
}
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StackSpec) DeepCopyInto(out *StackSpec) {
*out = *in
return
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new StackSpec.
func (x *StackSpec) DeepCopy() *StackSpec {
if x == nil {
return nil
}
out := new(StackSpec)
x.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StackStatus) DeepCopyInto(out *StackStatus) {
*out = *in
return
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new StackStatus.
func (x *StackStatus) DeepCopy() *StackStatus {
if x == nil {
return nil
}
out := new(StackStatus)
x.DeepCopyInto(out)
return out
}

View File

@@ -0,0 +1,231 @@
// +build !ignore_autogenerated
// This file was autogenerated by deepcopy-gen. Do not edit it manually!
package compose
import (
conversion "k8s.io/apimachinery/pkg/conversion"
runtime "k8s.io/apimachinery/pkg/runtime"
reflect "reflect"
)
// Deprecated: register deep-copy functions.
func init() {
SchemeBuilder.Register(RegisterDeepCopies)
}
// Deprecated: RegisterDeepCopies adds deep-copy functions to the given scheme. Public
// to allow building arbitrary schemes.
func RegisterDeepCopies(scheme *runtime.Scheme) error {
return scheme.AddGeneratedDeepCopyFuncs(
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
in.(*ImpersonationConfig).DeepCopyInto(out.(*ImpersonationConfig))
return nil
}, InType: reflect.TypeOf(&ImpersonationConfig{})},
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
in.(*Owner).DeepCopyInto(out.(*Owner))
return nil
}, InType: reflect.TypeOf(&Owner{})},
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
in.(*OwnerList).DeepCopyInto(out.(*OwnerList))
return nil
}, InType: reflect.TypeOf(&OwnerList{})},
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
in.(*Stack).DeepCopyInto(out.(*Stack))
return nil
}, InType: reflect.TypeOf(&Stack{})},
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
in.(*StackList).DeepCopyInto(out.(*StackList))
return nil
}, InType: reflect.TypeOf(&StackList{})},
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
in.(*StackSpec).DeepCopyInto(out.(*StackSpec))
return nil
}, InType: reflect.TypeOf(&StackSpec{})},
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
in.(*StackStatus).DeepCopyInto(out.(*StackStatus))
return nil
}, InType: reflect.TypeOf(&StackStatus{})},
)
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ImpersonationConfig) DeepCopyInto(out *ImpersonationConfig) {
*out = *in
if in.Groups != nil {
in, out := &in.Groups, &out.Groups
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Extra != nil {
in, out := &in.Extra, &out.Extra
*out = make(map[string][]string, len(*in))
for key, val := range *in {
if val == nil {
(*out)[key] = nil
} else {
(*out)[key] = make([]string, len(val))
copy((*out)[key], val)
}
}
}
return
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new ImpersonationConfig.
func (x *ImpersonationConfig) DeepCopy() *ImpersonationConfig {
if x == nil {
return nil
}
out := new(ImpersonationConfig)
x.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Owner) DeepCopyInto(out *Owner) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Owner.DeepCopyInto(&out.Owner)
return
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new Owner.
func (x *Owner) DeepCopy() *Owner {
if x == nil {
return nil
}
out := new(Owner)
x.DeepCopyInto(out)
return out
}
func (x *Owner) DeepCopyObject() runtime.Object {
if c := x.DeepCopy(); c != nil {
return c
} else {
return nil
}
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OwnerList) DeepCopyInto(out *OwnerList) {
*out = *in
out.TypeMeta = in.TypeMeta
out.ListMeta = in.ListMeta
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Owner, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new OwnerList.
func (x *OwnerList) DeepCopy() *OwnerList {
if x == nil {
return nil
}
out := new(OwnerList)
x.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Stack) DeepCopyInto(out *Stack) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
out.Status = in.Status
return
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new Stack.
func (x *Stack) DeepCopy() *Stack {
if x == nil {
return nil
}
out := new(Stack)
x.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (x *Stack) DeepCopyObject() runtime.Object {
if c := x.DeepCopy(); c != nil {
return c
} else {
return nil
}
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StackList) DeepCopyInto(out *StackList) {
*out = *in
out.TypeMeta = in.TypeMeta
out.ListMeta = in.ListMeta
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Stack, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new StackList.
func (x *StackList) DeepCopy() *StackList {
if x == nil {
return nil
}
out := new(StackList)
x.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (x *StackList) DeepCopyObject() runtime.Object {
if c := x.DeepCopy(); c != nil {
return c
} else {
return nil
}
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StackSpec) DeepCopyInto(out *StackSpec) {
*out = *in
in.Owner.DeepCopyInto(&out.Owner)
return
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new StackSpec.
func (x *StackSpec) DeepCopy() *StackSpec {
if x == nil {
return nil
}
out := new(StackSpec)
x.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StackStatus) DeepCopyInto(out *StackStatus) {
*out = *in
return
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new StackStatus.
func (x *StackStatus) DeepCopy() *StackStatus {
if x == nil {
return nil
}
out := new(StackStatus)
x.DeepCopyInto(out)
return out
}

View File

@@ -0,0 +1,4 @@
//
// +domain=docker.com
package apis

View File

@@ -0,0 +1,58 @@
package labels
import (
"fmt"
"strings"
)
const (
// ForServiceName is the label for the service name.
ForServiceName = "com.docker.service.name"
// ForStackName is the label for the stack name.
ForStackName = "com.docker.stack.namespace"
// ForServiceID is the label for the service id.
ForServiceID = "com.docker.service.id"
)
// ForService gives the labels to select a given service in a stack.
func ForService(stackName, serviceName string) map[string]string {
labels := map[string]string{}
if serviceName != "" {
labels[ForServiceName] = serviceName
}
if stackName != "" {
labels[ForStackName] = stackName
}
if serviceName != "" && stackName != "" {
labels[ForServiceID] = stackName + "-" + serviceName
}
return labels
}
// Merge merges multiple lists of labels.
func Merge(labelsList ...map[string]string) map[string]string {
merged := map[string]string{}
for _, labels := range labelsList {
for k, v := range labels {
merged[k] = v
}
}
return merged
}
// SelectorForStack gives the labelSelector to use for a given stack.
// Specific service names can be passed to narrow down the selection.
func SelectorForStack(stackName string, serviceNames ...string) string {
switch len(serviceNames) {
case 0:
return fmt.Sprintf("%s=%s", ForStackName, stackName)
case 1:
return fmt.Sprintf("%s=%s,%s=%s", ForStackName, stackName, ForServiceName, serviceNames[0])
default:
return fmt.Sprintf("%s=%s,%s in (%s)", ForStackName, stackName, ForServiceName, strings.Join(serviceNames, ","))
}
}

View File

@@ -0,0 +1,22 @@
package labels
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestForService(t *testing.T) {
labels := ForService("stack", "service")
assert.Len(t, labels, 3)
assert.Equal(t, "stack", labels["com.docker.stack.namespace"])
assert.Equal(t, "service", labels["com.docker.service.name"])
assert.Equal(t, "stack-service", labels["com.docker.service.id"])
}
func TestSelectorForStack(t *testing.T) {
assert.Equal(t, "com.docker.stack.namespace=demostack", SelectorForStack("demostack"))
assert.Equal(t, "com.docker.stack.namespace=stack,com.docker.service.name=service", SelectorForStack("stack", "service"))
assert.Equal(t, "com.docker.stack.namespace=stack,com.docker.service.name in (service1,service2)", SelectorForStack("stack", "service1", "service2"))
}

View File

@@ -0,0 +1,32 @@
package kubernetes
import (
"fmt"
apiv1beta1 "github.com/docker/cli/cli/command/stack/kubernetes/api/compose/v1beta1"
log "github.com/sirupsen/logrus"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
// APIPresent checks that an API is installed.
func APIPresent(config *rest.Config) error {
log.Debugf("check API present at %s", config.Host)
clients, err := kubernetes.NewForConfig(config)
if err != nil {
return err
}
groups, err := clients.Discovery().ServerGroups()
if err != nil {
return err
}
for _, group := range groups.Groups {
if group.Name == apiv1beta1.SchemeGroupVersion.Group {
return nil
}
}
return fmt.Errorf("could not find %s api. Install it on your cluster first", apiv1beta1.SchemeGroupVersion.Group)
}

View File

@@ -0,0 +1,91 @@
package kubernetes
import (
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/client-go/discovery"
"k8s.io/client-go/kubernetes"
appsv1beta2 "k8s.io/client-go/kubernetes/typed/apps/v1beta2"
typesappsv1beta2 "k8s.io/client-go/kubernetes/typed/apps/v1beta2"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
restclient "k8s.io/client-go/rest"
)
// Factory is the kubernetes client factory
type Factory struct {
namespace string
config *restclient.Config
coreClientSet *corev1.CoreV1Client
appsClientSet *appsv1beta2.AppsV1beta2Client
}
// NewFactory creates a kubernetes client factory
func NewFactory(namespace string, config *restclient.Config) (*Factory, error) {
coreClientSet, err := corev1.NewForConfig(config)
if err != nil {
return nil, err
}
appsClientSet, err := appsv1beta2.NewForConfig(config)
if err != nil {
return nil, err
}
return &Factory{
namespace: namespace,
config: config,
coreClientSet: coreClientSet,
appsClientSet: appsClientSet,
}, nil
}
// RESTMapper returns a PriorityRESTMapper based on the discovered
// groups and resources passed in.
func (s *Factory) RESTMapper() (meta.RESTMapper, error) {
clientSet, err := kubernetes.NewForConfig(s.config)
if err != nil {
return nil, err
}
groupResources, err := discovery.GetAPIGroupResources(clientSet.DiscoveryClient)
if err != nil {
return nil, err
}
mapper := discovery.NewRESTMapper(groupResources, meta.InterfacesForUnstructured)
return mapper, nil
}
// ConfigMaps returns a client for kubernetes's config maps
func (s *Factory) ConfigMaps() corev1.ConfigMapInterface {
return s.coreClientSet.ConfigMaps(s.namespace)
}
// Secrets returns a client for kubernetes's secrets
func (s *Factory) Secrets() corev1.SecretInterface {
return s.coreClientSet.Secrets(s.namespace)
}
// Services returns a client for kubernetes's secrets
func (s *Factory) Services() corev1.ServiceInterface {
return s.coreClientSet.Services(s.namespace)
}
// Pods returns a client for kubernetes's pods
func (s *Factory) Pods() corev1.PodInterface {
return s.coreClientSet.Pods(s.namespace)
}
// Nodes returns a client for kubernetes's nodes
func (s *Factory) Nodes() corev1.NodeInterface {
return s.coreClientSet.Nodes()
}
// ReplicationControllers returns a client for kubernetes replication controllers
func (s *Factory) ReplicationControllers() corev1.ReplicationControllerInterface {
return s.coreClientSet.ReplicationControllers(s.namespace)
}
// ReplicaSets return a client for kubernetes replace sets
func (s *Factory) ReplicaSets() typesappsv1beta2.ReplicaSetInterface {
return s.appsClientSet.ReplicaSets(s.namespace)
}

View File

@@ -0,0 +1,107 @@
package kubernetes
import (
"os"
"path/filepath"
"github.com/docker/cli/cli/command"
composev1beta1 "github.com/docker/cli/cli/command/stack/kubernetes/api/client/clientset_generated/clientset/typed/compose/v1beta1"
"github.com/docker/docker/pkg/homedir"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
// AddStackCommands adds `stack` subcommands
func AddStackCommands(root *cobra.Command, dockerCli command.Cli) {
var kubeCli kubeCli
configureCommand(root, &kubeCli)
root.AddCommand(
newDeployCommand(dockerCli, &kubeCli),
newListCommand(dockerCli, &kubeCli),
newRemoveCommand(dockerCli, &kubeCli),
newServicesCommand(dockerCli, &kubeCli),
newPsCommand(dockerCli, &kubeCli),
)
}
// NewTopLevelDeployCommand returns a command for `docker deploy`
func NewTopLevelDeployCommand(dockerCli command.Cli) *cobra.Command {
var kubeCli kubeCli
cmd := newDeployCommand(dockerCli, &kubeCli)
configureCommand(cmd, &kubeCli)
return cmd
}
func configureCommand(root *cobra.Command, kubeCli *kubeCli) {
var (
kubeOpts kubeOptions
)
kubeOpts.installFlags(root.PersistentFlags())
preRunE := root.PersistentPreRunE
root.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
if preRunE != nil {
if err := preRunE(cmd, args); err != nil {
return err
}
}
kubeCli.kubeNamespace = kubeOpts.namespace
if kubeCli.kubeNamespace == "" {
kubeCli.kubeNamespace = "default"
}
// Read kube config flag and environment variable
if kubeOpts.kubeconfig == "" {
if config := os.Getenv("KUBECONFIG"); config != "" {
kubeOpts.kubeconfig = config
} else {
kubeOpts.kubeconfig = filepath.Join(homedir.Get(), ".kube/config")
}
}
config, err := clientcmd.BuildConfigFromFlags("", kubeOpts.kubeconfig)
if err != nil {
return err
}
kubeCli.kubeConfig = config
return nil
}
}
// KubeOptions are options specific to kubernetes
type kubeOptions struct {
namespace string
kubeconfig string
}
// InstallFlags adds flags for the common options on the FlagSet
func (opts *kubeOptions) installFlags(flags *pflag.FlagSet) {
flags.StringVar(&opts.namespace, "namespace", "default", "Kubernetes namespace to use")
flags.StringVar(&opts.kubeconfig, "kubeconfig", "", "Kubernetes config file")
}
type kubeCli struct {
kubeConfig *restclient.Config
kubeNamespace string
}
func (c *kubeCli) ComposeClient() (*Factory, error) {
return NewFactory(c.kubeNamespace, c.kubeConfig)
}
func (c *kubeCli) KubeConfig() *restclient.Config {
return c.kubeConfig
}
func (c *kubeCli) Stacks() (composev1beta1.StackInterface, error) {
err := APIPresent(c.kubeConfig)
if err != nil {
return nil, err
}
clientSet, err := composev1beta1.NewForConfig(c.kubeConfig)
if err != nil {
return nil, err
}
return clientSet.Stacks(c.kubeNamespace), nil
}

View File

@@ -0,0 +1,71 @@
package kubernetes
import (
"fmt"
"sort"
apiv1beta1 "github.com/docker/cli/cli/command/stack/kubernetes/api/compose/v1beta1"
"github.com/docker/cli/cli/command/stack/kubernetes/api/labels"
"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/cli/compose/types"
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) error {
stackObjects, err := getServices(stack.Spec.ComposeFile)
if err != nil {
return err
}
for _, srv := range stackObjects {
if err := verify(services, stack.Name, srv); err != nil {
return err
}
}
return nil
}
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(composeFile string) ([]string, error) {
parsed, err := loader.ParseYAML([]byte(composeFile))
if err != nil {
return nil, err
}
config, err := loader.Load(types.ConfigDetails{
WorkingDir: ".",
ConfigFiles: []types.ConfigFile{
{
Config: parsed,
},
},
})
if err != nil {
return nil, err
}
services := make([]string, len(config.Services))
for i := range config.Services {
services[i] = config.Services[i].Name
}
sort.Strings(services)
return services, nil
}

View File

@@ -0,0 +1,171 @@
package kubernetes
import (
"fmt"
"time"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/stack/kubernetes/api/labels"
"github.com/docker/docker/api/types/swarm"
appsv1beta2 "k8s.io/api/apps/v1beta2"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
)
// Pod conversion
func podToTask(pod apiv1.Pod) swarm.Task {
var startTime time.Time
if pod.Status.StartTime != nil {
startTime = (*pod.Status.StartTime).Time
}
task := swarm.Task{
ID: string(pod.UID),
NodeID: pod.Spec.NodeName,
Spec: swarm.TaskSpec{
ContainerSpec: &swarm.ContainerSpec{
Image: getContainerImage(pod.Spec.Containers),
},
},
DesiredState: podPhaseToState(pod.Status.Phase),
Status: swarm.TaskStatus{
State: podPhaseToState(pod.Status.Phase),
Timestamp: startTime,
PortStatus: swarm.PortStatus{
Ports: getPorts(pod.Spec.Containers),
},
},
}
pod.GetLabels()
return task
}
func podPhaseToState(phase apiv1.PodPhase) swarm.TaskState {
switch phase {
case apiv1.PodPending:
return swarm.TaskStatePending
case apiv1.PodRunning:
return swarm.TaskStateRunning
case apiv1.PodSucceeded:
return swarm.TaskStateComplete
case apiv1.PodFailed:
return swarm.TaskStateFailed
default:
return swarm.TaskState("unknown")
}
}
func toSwarmProtocol(protocol apiv1.Protocol) swarm.PortConfigProtocol {
switch protocol {
case apiv1.ProtocolTCP:
return swarm.PortConfigProtocolTCP
case apiv1.ProtocolUDP:
return swarm.PortConfigProtocolUDP
}
return swarm.PortConfigProtocol("unknown")
}
func fetchPods(namespace string, pods corev1.PodInterface) ([]apiv1.Pod, error) {
labelSelector := labels.SelectorForStack(namespace)
podsList, err := pods.List(metav1.ListOptions{LabelSelector: labelSelector})
if err != nil {
return nil, err
}
return podsList.Items, nil
}
func getContainerImage(containers []apiv1.Container) string {
if len(containers) == 0 {
return ""
}
return containers[0].Image
}
func getPorts(containers []apiv1.Container) []swarm.PortConfig {
if len(containers) == 0 || len(containers[0].Ports) == 0 {
return nil
}
ports := make([]swarm.PortConfig, len(containers[0].Ports))
for i, port := range containers[0].Ports {
ports[i] = swarm.PortConfig{
PublishedPort: uint32(port.HostPort),
TargetPort: uint32(port.ContainerPort),
Protocol: toSwarmProtocol(port.Protocol),
}
}
return ports
}
type tasksBySlot []swarm.Task
func (t tasksBySlot) Len() int {
return len(t)
}
func (t tasksBySlot) Swap(i, j int) {
t[i], t[j] = t[j], t[i]
}
func (t tasksBySlot) Less(i, j int) bool {
// Sort by slot.
if t[i].Slot != t[j].Slot {
return t[i].Slot < t[j].Slot
}
// If same slot, sort by most recent.
return t[j].Meta.CreatedAt.Before(t[i].CreatedAt)
}
// Replicas conversion
func replicasToServices(replicas *appsv1beta2.ReplicaSetList, services *apiv1.ServiceList) ([]swarm.Service, map[string]formatter.ServiceListInfo, error) {
result := make([]swarm.Service, len(replicas.Items))
infos := make(map[string]formatter.ServiceListInfo, len(replicas.Items))
for i, r := range replicas.Items {
service, ok := findService(services, r.Labels[labels.ForServiceName])
if !ok {
return nil, nil, fmt.Errorf("could not find service '%s'", r.Labels[labels.ForServiceName])
}
uid := string(service.UID)
s := swarm.Service{
ID: uid,
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{
Name: service.Name,
},
TaskTemplate: swarm.TaskSpec{
ContainerSpec: &swarm.ContainerSpec{
Image: getContainerImage(r.Spec.Template.Spec.Containers),
},
},
},
}
if service.Spec.Type == apiv1.ServiceTypeLoadBalancer {
configs := make([]swarm.PortConfig, len(service.Spec.Ports))
for i, p := range service.Spec.Ports {
configs[i] = swarm.PortConfig{
PublishMode: swarm.PortConfigPublishModeIngress,
PublishedPort: uint32(p.Port),
TargetPort: uint32(p.TargetPort.IntValue()),
Protocol: toSwarmProtocol(p.Protocol),
}
}
s.Endpoint = swarm.Endpoint{Ports: configs}
}
result[i] = s
infos[uid] = formatter.ServiceListInfo{
Mode: "replicated",
Replicas: fmt.Sprintf("%d/%d", r.Status.AvailableReplicas, r.Status.Replicas),
}
}
return result, infos, nil
}
func findService(services *apiv1.ServiceList, name string) (apiv1.Service, bool) {
for _, s := range services.Items {
if s.Name == name {
return s, true
}
}
return apiv1.Service{}, false
}

View File

@@ -0,0 +1,41 @@
package kubernetes
import (
"github.com/docker/cli/cli/command/stack/kubernetes/api/labels"
apiv1 "k8s.io/api/core/v1"
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,
},
},
Data: map[string]string{
key: string(content),
},
}
}
// 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,
},
}
}

View File

@@ -0,0 +1,158 @@
package kubernetes
import (
"fmt"
"io/ioutil"
"path"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/stack/common"
composeTypes "github.com/docker/cli/cli/compose/types"
"github.com/pkg/errors"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
)
type deployOptions struct {
composefile string
stack string
}
func newDeployCommand(dockerCli command.Cli, kubeCli *kubeCli) *cobra.Command {
var opts deployOptions
cmd := &cobra.Command{
Use: "deploy [OPTIONS] STACK",
Aliases: []string{"up"},
Short: "Deploy a new stack or update an existing stack",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.stack = args[0]
return runDeploy(dockerCli, kubeCli, opts)
},
}
flags := cmd.Flags()
common.AddComposefileFlag(&opts.composefile, flags)
// FIXME(vdemeester) other flags ? (bundlefile, registry-auth, prune, resolve-image) ?
return cmd
}
func runDeploy(dockerCli command.Cli, kubeCli *kubeCli, opts deployOptions) error {
cmdOut := dockerCli.Out()
// Check arguments
if opts.composefile == "" {
return errors.Errorf("Please specify a Compose file (with --compose-file).")
}
// Initialize clients
stacks, err := kubeCli.Stacks()
if err != nil {
return err
}
composeClient, err := kubeCli.ComposeClient()
if err != nil {
return err
}
configMaps := composeClient.ConfigMaps()
secrets := composeClient.Secrets()
services := composeClient.Services()
pods := composeClient.Pods()
watcher := DeployWatcher{
Pods: pods,
}
// Parse the compose file
stack, cfg, err := LoadStack(opts.stack, opts.composefile)
if err != nil {
return err
}
// FIXME(vdemeester) handle warnings server-side
if err = IsColliding(services, stack); err != nil {
return err
}
if err = createFileBasedConfigMaps(stack.Name, cfg.Configs, configMaps); err != nil {
return err
}
if err = createFileBasedSecrets(stack.Name, cfg.Secrets, 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)
}
fmt.Fprintln(cmdOut, "Waiting for the stack to be stable and running...")
<-watcher.Watch(stack, serviceNames(cfg))
fmt.Fprintf(cmdOut, "Stack %s is stable and running\n\n", stack.Name)
// 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
}
configMap := toConfigMap(stackName, name, fileName, content)
configMaps.Create(configMap)
}
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
}
secret := toSecret(stackName, name, fileName, content)
secrets.Create(secret)
}
return nil
}

View File

@@ -0,0 +1,81 @@
package kubernetes
import (
"sort"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"vbom.ml/util/sortorder"
)
type listOptions struct {
format string
}
func newListCommand(dockerCli command.Cli, kubeCli *kubeCli) *cobra.Command {
opts := listOptions{}
cmd := &cobra.Command{
Use: "ls",
Aliases: []string{"list"},
Short: "List stacks",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runList(dockerCli, kubeCli, opts)
},
}
flags := cmd.Flags()
flags.StringVar(&opts.format, "format", "", "Pretty-print stacks using a Go template")
return cmd
}
func runList(dockerCli command.Cli, kubeCli *kubeCli, opts listOptions) error {
stacks, err := getStacks(kubeCli)
if err != nil {
return err
}
format := opts.format
if len(format) == 0 {
format = formatter.TableFormatKey
}
stackCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewStackFormat(format),
}
sort.Sort(byName(stacks))
return formatter.StackWrite(stackCtx, stacks)
}
type byName []*formatter.Stack
func (n byName) Len() int { return len(n) }
func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
func (n byName) Less(i, j int) bool { return sortorder.NaturalLess(n[i].Name, n[j].Name) }
func getStacks(kubeCli *kubeCli) ([]*formatter.Stack, error) {
stackSvc, err := kubeCli.Stacks()
if err != nil {
return nil, err
}
stacks, err := stackSvc.List(metav1.ListOptions{})
if err != nil {
return nil, err
}
var formattedStacks []*formatter.Stack
for _, stack := range stacks.Items {
services, err := getServices(stack.Spec.ComposeFile)
if err != nil {
return nil, err
}
formattedStacks = append(formattedStacks, &formatter.Stack{
Name: stack.Name,
Services: len(services),
})
}
return formattedStacks, nil
}

View File

@@ -0,0 +1,167 @@
package kubernetes
import (
"bufio"
"io/ioutil"
"os"
"path/filepath"
"strings"
apiv1beta1 "github.com/docker/cli/cli/command/stack/kubernetes/api/compose/v1beta1"
"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/cli/compose/template"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/pkg/errors"
yaml "gopkg.in/yaml.v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// LoadStack loads a stack from a Compose file, with a given name.
func LoadStack(name, composeFile string) (*apiv1beta1.Stack, *composetypes.Config, error) {
if composeFile == "" {
return nil, nil, errors.New("compose-file must be set")
}
workingDir, err := os.Getwd()
if err != nil {
return nil, nil, err
}
composePath := composeFile
if !strings.HasPrefix(composePath, "/") {
composePath = filepath.Join(workingDir, composeFile)
}
if _, err := os.Stat(composePath); os.IsNotExist(err) {
return nil, nil, errors.Errorf("no compose file found in %s", filepath.Dir(composePath))
}
binary, err := ioutil.ReadFile(composePath)
if err != nil {
return nil, nil, errors.Wrap(err, "cannot load compose file")
}
return load(name, binary, env())
}
func load(name string, binary []byte, env map[string]string) (*apiv1beta1.Stack, *composetypes.Config, error) {
processed, err := template.Substitute(string(binary), func(key string) (string, bool) { return env[key], true })
if err != nil {
return nil, nil, errors.Wrap(err, "cannot load compose file")
}
parsed, err := loader.ParseYAML([]byte(processed))
if err != nil {
return nil, nil, errors.Wrapf(err, "cannot load compose file")
}
cfg, err := loader.Load(composetypes.ConfigDetails{
WorkingDir: ".",
ConfigFiles: []composetypes.ConfigFile{
{
Config: parsed,
},
},
})
if err != nil {
return nil, nil, errors.Wrapf(err, "cannot load compose file")
}
result, err := processEnvFiles(processed, parsed, cfg)
if err != nil {
return nil, nil, errors.Wrapf(err, "cannot load compose file")
}
return &apiv1beta1.Stack{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: apiv1beta1.StackSpec{
ComposeFile: result,
},
}, cfg, nil
}
type iMap = map[string]interface{}
func processEnvFiles(input string, parsed map[string]interface{}, config *composetypes.Config) (string, error) {
changed := false
for _, svc := range config.Services {
if len(svc.EnvFile) == 0 {
continue
}
// Load() processed the env_file for us, we just need to inject back into
// the intermediate representation
env := iMap{}
for k, v := range svc.Environment {
env[k] = v
}
parsed["services"].(iMap)[svc.Name].(iMap)["environment"] = env
delete(parsed["services"].(iMap)[svc.Name].(iMap), "env_file")
changed = true
}
if !changed {
return input, nil
}
res, err := yaml.Marshal(parsed)
if err != nil {
return "", err
}
return string(res), nil
}
func env() map[string]string {
// Apply .env file first
config := readEnvFile(".env")
// Apply env variables
for k, v := range envToMap(os.Environ()) {
config[k] = v
}
return config
}
func readEnvFile(path string) map[string]string {
config := map[string]string{}
file, err := os.Open(path)
if err != nil {
return config // Ignore
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(strings.TrimSpace(line), "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
key := parts[0]
value := parts[1]
config[key] = value
}
}
return config
}
func envToMap(env []string) map[string]string {
config := map[string]string{}
for _, value := range env {
parts := strings.SplitN(value, "=", 2)
key := parts[0]
value := parts[1]
config[key] = value
}
return config
}

View File

@@ -0,0 +1,34 @@
package kubernetes
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPlaceholders(t *testing.T) {
env := map[string]string{
"TAG": "_latest_",
"K1": "V1",
"K2": "V2",
}
prefix := "version: '3'\nvolumes:\n data:\n external:\n name: "
var tests = []struct {
input string
expectedOutput string
}{
{prefix + "BEFORE${TAG}AFTER", prefix + "BEFORE_latest_AFTER"},
{prefix + "BEFORE${K1}${K2}AFTER", prefix + "BEFOREV1V2AFTER"},
{prefix + "BEFORE$TAG AFTER", prefix + "BEFORE_latest_ AFTER"},
{prefix + "BEFORE$$TAG AFTER", prefix + "BEFORE$TAG AFTER"},
{prefix + "BEFORE $UNKNOWN AFTER", prefix + "BEFORE AFTER"},
}
for _, test := range tests {
output, _, err := load("stack", []byte(test.input), env)
require.NoError(t, err)
assert.Equal(t, test.expectedOutput, output.Spec.ComposeFile)
}
}

View File

@@ -0,0 +1,138 @@
package kubernetes
import (
"fmt"
"sort"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/task"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types/swarm"
"github.com/spf13/cobra"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/kubernetes/pkg/api"
)
type psOptions struct {
filter opts.FilterOpt
noTrunc bool
namespace string
noResolve bool
quiet bool
format string
}
func newPsCommand(dockerCli command.Cli, kubeCli *kubeCli) *cobra.Command {
options := psOptions{filter: opts.NewFilterOpt()}
cmd := &cobra.Command{
Use: "ps [OPTIONS] STACK",
Short: "List the tasks in the stack",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
options.namespace = args[0]
return runPS(dockerCli, kubeCli, options)
},
}
flags := cmd.Flags()
flags.BoolVar(&options.noTrunc, "no-trunc", false, "Do not truncate output")
flags.BoolVar(&options.noResolve, "no-resolve", false, "Do not map IDs to Names")
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display task IDs")
flags.StringVar(&options.format, "format", "", "Pretty-print tasks using a Go template")
return cmd
}
func runPS(dockerCli command.Cli, kubeCli *kubeCli, options psOptions) error {
namespace := options.namespace
// Initialize clients
client, err := kubeCli.ComposeClient()
if err != nil {
return err
}
stacks, err := kubeCli.Stacks()
if err != nil {
return err
}
podsClient := client.Pods()
// Fetch pods
if _, err := stacks.Get(namespace, metav1.GetOptions{}); err != nil {
return fmt.Errorf("nothing found in stack: %s", namespace)
}
pods, err := fetchPods(namespace, podsClient)
if err != nil {
return err
}
if len(pods) == 0 {
return fmt.Errorf("nothing found in stack: %s", namespace)
}
format := options.format
if len(format) == 0 {
format = task.DefaultFormat(dockerCli.ConfigFile(), options.quiet)
}
nodeResolver := makeNodeResolver(options.noResolve, client.Nodes())
tasks := make([]swarm.Task, len(pods))
for i, pod := range pods {
tasks[i] = podToTask(pod)
}
return print(dockerCli, tasks, pods, nodeResolver, !options.noTrunc, options.quiet, format)
}
type idResolver func(name string) (string, error)
func print(dockerCli command.Cli, tasks []swarm.Task, pods []apiv1.Pod, nodeResolver idResolver, trunc, quiet bool, format string) error {
sort.Stable(tasksBySlot(tasks))
names := map[string]string{}
nodes := map[string]string{}
tasksCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewTaskFormat(format, quiet),
Trunc: trunc,
}
for i, task := range tasks {
nodeValue, err := nodeResolver(pods[i].Spec.NodeName)
if err != nil {
return err
}
names[task.ID] = pods[i].Name
nodes[task.ID] = nodeValue
}
return formatter.TaskWrite(tasksCtx, tasks, names, nodes)
}
func makeNodeResolver(noResolve bool, nodes corev1.NodeInterface) func(string) (string, error) {
// Here we have a name and we need to resolve its identifier. To mimic swarm behavior
// we need to resolve the id when noresolve is set, otherwise we return the name.
if noResolve {
return func(name string) (string, error) {
n, err := nodes.List(metav1.ListOptions{
FieldSelector: fields.OneTermEqualSelector(api.ObjectNameField, name).String(),
})
if err != nil {
return "", err
}
if len(n.Items) != 1 {
return "", fmt.Errorf("could not find node '%s'", name)
}
return string(n.Items[0].UID), nil
}
}
return func(name string) (string, error) { return name, nil }
}

View File

@@ -0,0 +1,46 @@
package kubernetes
import (
"fmt"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type removeOptions struct {
stacks []string
}
func newRemoveCommand(dockerCli command.Cli, kubeCli *kubeCli) *cobra.Command {
var opts removeOptions
cmd := &cobra.Command{
Use: "rm STACK [STACK...]",
Aliases: []string{"remove", "down"},
Short: "Remove one or more stacks",
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.stacks = args
return runRemove(dockerCli, kubeCli, opts)
},
}
return cmd
}
func runRemove(dockerCli command.Cli, kubeCli *kubeCli, opts removeOptions) error {
stacks, err := kubeCli.Stacks()
if err != nil {
return err
}
for _, stack := range opts.stacks {
fmt.Fprintf(dockerCli.Out(), "Removing stack: %s\n", stack)
err := stacks.Delete(stack, &metav1.DeleteOptions{})
if err != nil {
fmt.Fprintf(dockerCli.Out(), "Failed to remove stack %s: %s\n", stack, err)
return err
}
}
return nil
}

View File

@@ -0,0 +1,90 @@
package kubernetes
import (
"fmt"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/stack/kubernetes/api/labels"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type servicesOptions struct {
quiet bool
format string
namespace string
}
func newServicesCommand(dockerCli command.Cli, kubeCli *kubeCli) *cobra.Command {
var options servicesOptions
cmd := &cobra.Command{
Use: "services [OPTIONS] STACK",
Short: "List the services in the stack",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
options.namespace = args[0]
return runServices(dockerCli, kubeCli, options)
},
}
flags := cmd.Flags()
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display IDs")
flags.StringVar(&options.format, "format", "", "Pretty-print services using a Go template")
return cmd
}
func runServices(dockerCli command.Cli, kubeCli *kubeCli, options servicesOptions) error {
// Initialize clients
client, err := kubeCli.ComposeClient()
if err != nil {
return nil
}
stacks, err := kubeCli.Stacks()
if err != nil {
return err
}
replicas := client.ReplicaSets()
if _, err := stacks.Get(options.namespace, metav1.GetOptions{}); err != nil {
fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", options.namespace)
return nil
}
replicasList, err := replicas.List(metav1.ListOptions{LabelSelector: labels.SelectorForStack(options.namespace)})
if err != nil {
return err
}
servicesList, err := client.Services().List(metav1.ListOptions{LabelSelector: labels.SelectorForStack(options.namespace)})
if err != nil {
return err
}
// Convert Replicas sets and kubernetes services to swam services and formatter informations
services, info, err := replicasToServices(replicasList, servicesList)
if err != nil {
return err
}
if options.quiet {
info = map[string]formatter.ServiceListInfo{}
}
format := options.format
if len(format) == 0 {
if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !options.quiet {
format = dockerCli.ConfigFile().ServicesFormat
} else {
format = formatter.TableFormatKey
}
}
servicesCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewServiceListFormat(format, options.quiet),
}
return formatter.ServiceListWrite(servicesCtx, services, info)
}

View File

@@ -0,0 +1,117 @@
package kubernetes
import (
"fmt"
"time"
apiv1beta1 "github.com/docker/cli/cli/command/stack/kubernetes/api/compose/v1beta1"
"github.com/docker/cli/cli/command/stack/kubernetes/api/labels"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
)
// DeployWatcher watches a stack deployement
type DeployWatcher struct {
Pods corev1.PodInterface
}
// 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 {
stop := make(chan bool)
go w.waitForPods(stack.Name, serviceNames, stop)
return stop
}
func (w DeployWatcher) waitForPods(stackName string, serviceNames []string, stop chan bool) {
starts := map[string]int32{}
for {
time.Sleep(1 * time.Second)
list, err := w.Pods.List(metav1.ListOptions{
LabelSelector: labels.SelectorForStack(stackName),
IncludeUninitialized: true,
})
if err != nil {
stop <- true
return
}
for i := range list.Items {
pod := list.Items[i]
if pod.Status.Phase != apiv1.PodRunning {
continue
}
startCount := startCount(pod)
serviceName := pod.Labels[labels.ForServiceName]
if startCount != starts[serviceName] {
if startCount == 1 {
fmt.Printf(" - Service %s has one container running\n", serviceName)
} else {
fmt.Printf(" - Service %s was restarted %d %s\n", serviceName, startCount-1, timeTimes(startCount-1))
}
starts[serviceName] = startCount
}
}
if allReady(list.Items, serviceNames) {
stop <- true
return
}
}
}
func startCount(pod apiv1.Pod) int32 {
restart := int32(0)
for _, status := range pod.Status.ContainerStatuses {
restart += status.RestartCount
}
return 1 + restart
}
func allReady(pods []apiv1.Pod, serviceNames []string) bool {
serviceUp := map[string]bool{}
for _, pod := range pods {
if time.Since(pod.GetCreationTimestamp().Time) < 10*time.Second {
return false
}
ready := false
for _, cond := range pod.Status.Conditions {
if cond.Type == apiv1.PodReady && cond.Status == apiv1.ConditionTrue {
ready = true
}
}
if !ready {
return false
}
serviceName := pod.Labels[labels.ForServiceName]
serviceUp[serviceName] = true
}
for _, serviceName := range serviceNames {
if !serviceUp[serviceName] {
return false
}
}
return true
}
func timeTimes(n int32) string {
if n == 1 {
return "time"
}
return "times"
}

View File

@@ -1,4 +1,4 @@
package stack
package swarm
import (
"strings"

View File

@@ -0,0 +1,22 @@
package swarm
import (
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
// AddStackCommands adds `stack` subcommands
func AddStackCommands(root *cobra.Command, dockerCli command.Cli) {
root.AddCommand(
newDeployCommand(dockerCli),
newListCommand(dockerCli),
newRemoveCommand(dockerCli),
newServicesCommand(dockerCli),
newPsCommand(dockerCli),
)
}
// NewTopLevelDeployCommand returns a command for `docker deploy`
func NewTopLevelDeployCommand(dockerCli command.Cli) *cobra.Command {
return newDeployCommand(dockerCli)
}

View File

@@ -1,4 +1,4 @@
package stack
package swarm
import (
"github.com/docker/cli/cli/compose/convert"

View File

@@ -1,10 +1,11 @@
package stack
package swarm
import (
"fmt"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/stack/common"
"github.com/docker/cli/cli/compose/convert"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions"
@@ -44,9 +45,9 @@ func newDeployCommand(dockerCli command.Cli) *cobra.Command {
}
flags := cmd.Flags()
addBundlefileFlag(&opts.bundlefile, flags)
addComposefileFlag(&opts.composefile, flags)
addRegistryAuthFlag(&opts.sendRegistryAuth, flags)
common.AddBundlefileFlag(&opts.bundlefile, flags)
common.AddComposefileFlag(&opts.composefile, flags)
common.AddRegistryAuthFlag(&opts.sendRegistryAuth, flags)
flags.BoolVar(&opts.prune, "prune", false, "Prune services that are no longer referenced")
flags.SetAnnotation("prune", "version", []string{"1.27"})
flags.StringVar(&opts.resolveImage, "resolve-image", resolveImageAlways,

View File

@@ -1,16 +1,17 @@
package stack
package swarm
import (
"golang.org/x/net/context"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/stack/common"
"github.com/docker/cli/cli/compose/convert"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
)
func deployBundle(ctx context.Context, dockerCli command.Cli, opts deployOptions) error {
bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile)
bundle, err := common.LoadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile)
if err != nil {
return err
}

View File

@@ -1,4 +1,4 @@
package stack
package swarm
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package stack
package swarm
import (
"os"

View File

@@ -1,4 +1,4 @@
package stack
package swarm
import (
"testing"

View File

@@ -1,4 +1,4 @@
package stack
package swarm
import (
"sort"

View File

@@ -1,4 +1,4 @@
package stack
package swarm
import (
"io/ioutil"

View File

@@ -1,4 +1,4 @@
package stack
package swarm
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package stack
package swarm
import (
"io/ioutil"

View File

@@ -1,4 +1,4 @@
package stack
package swarm
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package stack
package swarm
import (
"errors"

View File

@@ -1,4 +1,4 @@
package stack
package swarm
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package stack
package swarm
import (
"io/ioutil"