diff --git a/cmd/kind/get/clusters/clusters.go b/cmd/kind/get/clusters/clusters.go new file mode 100644 index 0000000000..e2a1ef9781 --- /dev/null +++ b/cmd/kind/get/clusters/clusters.go @@ -0,0 +1,52 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package clusters implements the `clusters` command +package clusters + +import ( + "fmt" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "sigs.k8s.io/kind/pkg/cluster" +) + +// NewCommand returns a new cobra.Command for getting the list of clusters +func NewCommand() *cobra.Command { + cmd := &cobra.Command{ + // TODO(bentheelder): more detailed usage + Use: "clusters", + Short: "lists existing kind clusters by their name", + Long: "lists existing kind clusters by their name", + RunE: func(cmd *cobra.Command, args []string) error { + return runE(cmd, args) + }, + } + return cmd +} + +func runE(cmd *cobra.Command, args []string) error { + clusters, err := cluster.List() + if err != nil { + return errors.Wrap(err, "error listing clusters") + } + for _, cluster := range clusters { + fmt.Println(cluster.Name()) + } + return nil +} diff --git a/cmd/kind/get/get.go b/cmd/kind/get/get.go index c68232be81..0f41169bd5 100644 --- a/cmd/kind/get/get.go +++ b/cmd/kind/get/get.go @@ -20,6 +20,7 @@ package get import ( "github.com/spf13/cobra" + "sigs.k8s.io/kind/cmd/kind/get/clusters" "sigs.k8s.io/kind/cmd/kind/get/kubeconfigpath" ) @@ -28,13 +29,14 @@ func NewCommand() *cobra.Command { cmd := &cobra.Command{ // TODO(bentheelder): more detailed usage Use: "get", - Short: "Gets one of [kubeconfig-path]", - Long: "Gets one of [kubeconfig-path]", + Short: "Gets one of [clusters, kubeconfig-path]", + Long: "Gets one of [clusters, kubeconfig-path]", RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() }, } // add subcommands + cmd.AddCommand(clusters.NewCommand()) cmd.AddCommand(kubeconfigpath.NewCommand()) return cmd } diff --git a/pkg/cluster/clusters.go b/pkg/cluster/clusters.go new file mode 100644 index 0000000000..eae4e642d5 --- /dev/null +++ b/pkg/cluster/clusters.go @@ -0,0 +1,36 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cluster + +import ( + "github.com/pkg/errors" + + "sigs.k8s.io/kind/pkg/cluster/nodes" +) + +// List returns a list of clusters for which node containers exist +func List() ([]Context, error) { + n, err := nodes.ListByCluster() + if err != nil { + return nil, errors.Wrap(err, "could not list clusters, failed to list nodes") + } + clusters := []Context{} + for name := range n { + clusters = append(clusters, *newContextNoValidation(name)) + } + return clusters, nil +} diff --git a/pkg/cluster/cluster.go b/pkg/cluster/context.go similarity index 95% rename from pkg/cluster/cluster.go rename to pkg/cluster/context.go index 10628e0ff9..31c09d6aff 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/context.go @@ -53,6 +53,9 @@ var validNameRE = regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`) // NewContext returns a new cluster management context // if name is "" the default ("1") will be used func NewContext(name string) (ctx *Context, err error) { + // TODO(bentheelder): move validation out of NewContext and into create type + // calls, so that EG delete still works on previously valid, now invalid + // names if kind updates if name == "" { name = "1" } @@ -63,9 +66,17 @@ func NewContext(name string) (ctx *Context, err error) { name, validNameRE.String(), ) } + return newContextNoValidation(name), nil +} + +// internal helper that does the actual allocation consitently, but does not +// validate the cluster name +// we need this so that if we tighten the validation, other internal code +// can still create contexts to existing clusters by name (see List()) +func newContextNoValidation(name string) *Context { return &Context{ name: name, - }, nil + } } // ClusterLabel returns the docker object label that will be applied diff --git a/pkg/cluster/cluster_test.go b/pkg/cluster/context_test.go similarity index 100% rename from pkg/cluster/cluster_test.go rename to pkg/cluster/context_test.go diff --git a/pkg/cluster/nodes/nodes.go b/pkg/cluster/nodes/nodes.go index 9357c5a379..1b3baf5679 100644 --- a/pkg/cluster/nodes/nodes.go +++ b/pkg/cluster/nodes/nodes.go @@ -17,6 +17,9 @@ limitations under the License. package nodes import ( + "fmt" + "strings" + "github.com/pkg/errors" "sigs.k8s.io/kind/pkg/cluster/consts" @@ -52,8 +55,9 @@ func Delete(nodes ...*Node) error { func List(filters ...string) ([]*Node, error) { args := []string{ "ps", - "-q", // quiet output for parsing - "-a", // show stopped nodes + "-q", // quiet output for parsing + "-a", // show stopped nodes + "--no-trunc", // don't truncate // filter for nodes with the cluster label "--filter", "label=" + consts.ClusterLabelKey, } @@ -72,3 +76,33 @@ func List(filters ...string) ([]*Node, error) { } return nodes, nil } + +// ListByCluster returns a list of nodes by the kind cluster name +func ListByCluster() (map[string][]Node, error) { + args := []string{ + "ps", + "-q", // quiet output for parsing + "-a", // show stopped nodes + "--no-trunc", // don't truncate + // filter for nodes with the cluster label + "--filter", "label=" + consts.ClusterLabelKey, + // format to include friendly name and the cluster name + "--format", fmt.Sprintf(`{{.Names}}\t{{.Label "%s"}}`, consts.ClusterLabelKey), + } + cmd := exec.Command("docker", args...) + lines, err := exec.CombinedOutputLines(cmd) + if err != nil { + return nil, errors.Wrap(err, "failed to list nodes") + } + nodes := make(map[string][]Node) + for _, line := range lines { + parts := strings.Split(line, "\t") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid output when listing nodes: %s", line) + } + names := strings.Split(parts[0], ",") + cluster := parts[1] + nodes[cluster] = append(nodes[cluster], *FromID(names[0])) + } + return nodes, nil +}