Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better stack list experience with Kubernetes and Swarm #1031

Merged
merged 3 commits into from
May 15, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion cli/command/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,17 @@ type ClientInfo struct {

// HasKubernetes checks if kubernetes orchestrator is enabled
func (c ClientInfo) HasKubernetes() bool {
return c.HasExperimental && c.Orchestrator == OrchestratorKubernetes
return c.HasExperimental && (c.Orchestrator == OrchestratorKubernetes || c.Orchestrator == OrchestratorAll)
}

// HasSwarm checks if swarm orchestrator is enabled
func (c ClientInfo) HasSwarm() bool {
return c.Orchestrator == OrchestratorSwarm || c.Orchestrator == OrchestratorAll
}

// HasAll checks if all orchestrator is enabled
func (c ClientInfo) HasAll() bool {
return c.Orchestrator == OrchestratorAll
}

// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err.
Expand Down
19 changes: 19 additions & 0 deletions cli/command/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ func TestOrchestratorSwitch(t *testing.T) {
flagOrchestrator string
expectedOrchestrator string
expectedKubernetes bool
expectedSwarm bool
}{
{
doc: "default",
Expand All @@ -179,6 +180,7 @@ func TestOrchestratorSwitch(t *testing.T) {
}`,
expectedOrchestrator: "swarm",
expectedKubernetes: false,
expectedSwarm: true,
},
{
doc: "kubernetesIsExperimental",
Expand All @@ -190,6 +192,7 @@ func TestOrchestratorSwitch(t *testing.T) {
flagOrchestrator: "kubernetes",
expectedOrchestrator: "swarm",
expectedKubernetes: false,
expectedSwarm: true,
},
{
doc: "kubernetesConfigFile",
Expand All @@ -199,6 +202,7 @@ func TestOrchestratorSwitch(t *testing.T) {
}`,
expectedOrchestrator: "kubernetes",
expectedKubernetes: true,
expectedSwarm: false,
},
{
doc: "kubernetesEnv",
Expand All @@ -208,6 +212,7 @@ func TestOrchestratorSwitch(t *testing.T) {
envOrchestrator: "kubernetes",
expectedOrchestrator: "kubernetes",
expectedKubernetes: true,
expectedSwarm: false,
},
{
doc: "kubernetesFlag",
Expand All @@ -217,6 +222,17 @@ func TestOrchestratorSwitch(t *testing.T) {
flagOrchestrator: "kubernetes",
expectedOrchestrator: "kubernetes",
expectedKubernetes: true,
expectedSwarm: false,
},
{
doc: "allOrchestratorFlag",
configfile: `{
"experimental": "enabled"
}`,
flagOrchestrator: "all",
expectedOrchestrator: "all",
expectedKubernetes: true,
expectedSwarm: true,
},
{
doc: "envOverridesConfigFile",
Expand All @@ -227,6 +243,7 @@ func TestOrchestratorSwitch(t *testing.T) {
envOrchestrator: "swarm",
expectedOrchestrator: "swarm",
expectedKubernetes: false,
expectedSwarm: true,
},
{
doc: "flagOverridesEnv",
Expand All @@ -237,6 +254,7 @@ func TestOrchestratorSwitch(t *testing.T) {
flagOrchestrator: "swarm",
expectedOrchestrator: "swarm",
expectedKubernetes: false,
expectedSwarm: true,
},
}

Expand All @@ -260,6 +278,7 @@ func TestOrchestratorSwitch(t *testing.T) {
err := cli.Initialize(options)
assert.NilError(t, err)
assert.Check(t, is.Equal(testcase.expectedKubernetes, cli.ClientInfo().HasKubernetes()))
assert.Check(t, is.Equal(testcase.expectedSwarm, cli.ClientInfo().HasSwarm()))
assert.Check(t, is.Equal(testcase.expectedOrchestrator, string(cli.ClientInfo().Orchestrator)))
})
}
Expand Down
6 changes: 5 additions & 1 deletion cli/command/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const (
OrchestratorKubernetes = Orchestrator("kubernetes")
// OrchestratorSwarm orchestrator
OrchestratorSwarm = Orchestrator("swarm")
// OrchestratorAll orchestrator
OrchestratorAll = Orchestrator("all")
orchestratorUnset = Orchestrator("unset")

defaultOrchestrator = OrchestratorSwarm
Expand All @@ -27,8 +29,10 @@ func normalize(value string) (Orchestrator, error) {
return OrchestratorSwarm, nil
case "":
return orchestratorUnset, nil
case "all":
return OrchestratorAll, nil
default:
return defaultOrchestrator, fmt.Errorf("specified orchestrator %q is invalid, please use either kubernetes or swarm", value)
return defaultOrchestrator, fmt.Errorf("specified orchestrator %q is invalid, please use either kubernetes, swarm or all", value)
}
}

Expand Down
7 changes: 4 additions & 3 deletions cli/command/stack/cmd.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package stack

import (
"fmt"

"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)

var errUnsupportedAllOrchestrator = fmt.Errorf(`no orchestrator specified: use either "kubernetes" or "swarm"`)

// NewStackCommand returns a cobra command for `stack` subcommands
func NewStackCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Expand All @@ -27,9 +31,6 @@ func NewStackCommand(dockerCli command.Cli) *cobra.Command {
newServicesCommand(dockerCli),
)
flags := cmd.PersistentFlags()
flags.String("namespace", "", "Kubernetes namespace to use")
flags.SetAnnotation("namespace", "kubernetes", nil)
flags.SetAnnotation("namespace", "experimentalCLI", nil)
flags.String("kubeconfig", "", "Kubernetes config file")
flags.SetAnnotation("kubeconfig", "kubernetes", nil)
flags.SetAnnotation("kubeconfig", "experimentalCLI", nil)
Expand Down
9 changes: 7 additions & 2 deletions cli/command/stack/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@ func newDeployCommand(dockerCli command.Cli) *cobra.Command {
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.Namespace = args[0]
if dockerCli.ClientInfo().HasKubernetes() {
switch {
case dockerCli.ClientInfo().HasAll():
return errUnsupportedAllOrchestrator
case dockerCli.ClientInfo().HasKubernetes():
kli, err := kubernetes.WrapCli(dockerCli, kubernetes.NewOptions(cmd.Flags()))
if err != nil {
return err
}
return kubernetes.RunDeploy(kli, opts)
default:
return swarm.RunDeploy(dockerCli, opts)
}
return swarm.RunDeploy(dockerCli, opts)
},
}

Expand All @@ -45,5 +49,6 @@ func newDeployCommand(dockerCli command.Cli) *cobra.Command {
`Query the registry to resolve image digest and supported platforms ("`+swarm.ResolveImageAlways+`"|"`+swarm.ResolveImageChanged+`"|"`+swarm.ResolveImageNever+`")`)
flags.SetAnnotation("resolve-image", "version", []string{"1.30"})
flags.SetAnnotation("resolve-image", "swarm", nil)
kubernetes.AddNamespaceFlag(flags)
return cmd
}
8 changes: 7 additions & 1 deletion cli/command/stack/kubernetes/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,15 @@ func NewOptions(flags *flag.FlagSet) Options {
return opts
}

// AddNamespaceFlag adds the namespace flag to the given flag set
func AddNamespaceFlag(flags *flag.FlagSet) {
flags.String("namespace", "", "Kubernetes namespace to use")
flags.SetAnnotation("namespace", "kubernetes", nil)
flags.SetAnnotation("namespace", "experimentalCLI", nil)
}

// WrapCli wraps command.Cli with kubernetes specifics
func WrapCli(dockerCli command.Cli, opts Options) (*KubeCli, error) {
var err error
cli := &KubeCli{
Cli: dockerCli,
}
Expand Down
59 changes: 35 additions & 24 deletions cli/command/stack/kubernetes/list.go
Original file line number Diff line number Diff line change
@@ -1,44 +1,30 @@
package kubernetes

import (
"sort"

"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/stack/options"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"vbom.ml/util/sortorder"
)

// RunList is the kubernetes implementation of docker stack ls
func RunList(dockerCli *KubeCli, opts options.List) error {
stacks, err := getStacks(dockerCli, opts.AllNamespaces)
// GetStacks lists the kubernetes stacks
func GetStacks(dockerCli command.Cli, opts options.List, kopts Options) ([]*formatter.Stack, error) {
kubeCli, err := WrapCli(dockerCli, kopts)
if err != nil {
return err
}
format := opts.Format
if format == "" || format == formatter.TableFormatKey {
format = formatter.KubernetesStackTableFormat
return nil, err
}
stackCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.Format(format),
if opts.AllNamespaces || len(opts.Namespaces) == 0 {
return getStacks(kubeCli, opts)
}
sort.Sort(byName(stacks))
return formatter.StackWrite(stackCtx, stacks)
return getStacksWithNamespaces(kubeCli, opts)
}

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, allNamespaces bool) ([]*formatter.Stack, error) {
func getStacks(kubeCli *KubeCli, opts options.List) ([]*formatter.Stack, error) {
composeClient, err := kubeCli.composeClient()
if err != nil {
return nil, err
}
stackSvc, err := composeClient.Stacks(allNamespaces)
stackSvc, err := composeClient.Stacks(opts.AllNamespaces)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR, but we should consider passing an opts here, instead of a boolean (opts.AllNamespaces) to prevent future bloat in the signature

if err != nil {
return nil, err
}
Expand All @@ -57,3 +43,28 @@ func getStacks(kubeCli *KubeCli, allNamespaces bool) ([]*formatter.Stack, error)
}
return formattedStacks, nil
}

func getStacksWithNamespaces(kubeCli *KubeCli, opts options.List) ([]*formatter.Stack, error) {
stacks := []*formatter.Stack{}
for _, namespace := range removeDuplicates(opts.Namespaces) {
kubeCli.kubeNamespace = namespace
ss, err := getStacks(kubeCli, opts)
if err != nil {
return nil, err
}
stacks = append(stacks, ss...)
}
return stacks, nil
}

func removeDuplicates(namespaces []string) []string {
found := make(map[string]bool)
results := namespaces[:0]
for _, n := range namespaces {
if !found[n] {
results = append(results, n)
found[n] = true
}
}
return results
}
58 changes: 49 additions & 9 deletions cli/command/stack/list.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package stack

import (
"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/stack/kubernetes"
"github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/cli/command/stack/swarm"
"github.com/spf13/cobra"
"vbom.ml/util/sortorder"
)

func newListCommand(dockerCli command.Cli) *cobra.Command {
Expand All @@ -18,20 +22,56 @@ func newListCommand(dockerCli command.Cli) *cobra.Command {
Short: "List stacks",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if dockerCli.ClientInfo().HasKubernetes() {
kli, err := kubernetes.WrapCli(dockerCli, kubernetes.NewOptions(cmd.Flags()))
if err != nil {
return err
}
return kubernetes.RunList(kli, opts)
}
return swarm.RunList(dockerCli, opts)
return runList(cmd, dockerCli, opts)
},
}

flags := cmd.Flags()
flags.StringVar(&opts.Format, "format", "", "Pretty-print stacks using a Go template")
flags.BoolVarP(&opts.AllNamespaces, "all-namespaces", "", false, "List stacks among all Kubernetes namespaces")
flags.StringSliceVar(&opts.Namespaces, "namespace", []string{}, "Kubernetes namespaces to use")
flags.SetAnnotation("namespace", "kubernetes", nil)
flags.SetAnnotation("namespace", "experimentalCLI", nil)
flags.BoolVarP(&opts.AllNamespaces, "all-namespaces", "", false, "List stacks from all Kubernetes namespaces")
flags.SetAnnotation("all-namespaces", "kubernetes", nil)
flags.SetAnnotation("all-namespaces", "experimentalCLI", nil)
return cmd
}

func runList(cmd *cobra.Command, dockerCli command.Cli, opts options.List) error {
stacks := []*formatter.Stack{}
if dockerCli.ClientInfo().HasSwarm() {
ss, err := swarm.GetStacks(dockerCli)
if err != nil {
return err
}
stacks = append(stacks, ss...)
}
if dockerCli.ClientInfo().HasKubernetes() {
ss, err := kubernetes.GetStacks(dockerCli, opts, kubernetes.NewOptions(cmd.Flags()))
if err != nil {
return err
}
stacks = append(stacks, ss...)
}
return format(dockerCli, opts, stacks)
}

func format(dockerCli command.Cli, opts options.List, stacks []*formatter.Stack) error {
format := opts.Format
if format == "" || format == formatter.TableFormatKey {
format = formatter.SwarmStackTableFormat
if dockerCli.ClientInfo().HasKubernetes() {
format = formatter.KubernetesStackTableFormat
}
}
stackCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.Format(format),
}
sort.Slice(stacks, func(i, j int) bool {
return sortorder.NaturalLess(stacks[i].Name, stacks[j].Name) ||
!sortorder.NaturalLess(stacks[j].Name, stacks[i].Name) &&
sortorder.NaturalLess(stacks[j].Namespace, stacks[i].Namespace)
})
return formatter.StackWrite(stackCtx, stacks)
}
Loading