diff --git a/cmd/delete/deleteCluster.go b/cmd/delete/deleteCluster.go index 336bba73..29753f6f 100644 --- a/cmd/delete/deleteCluster.go +++ b/cmd/delete/deleteCluster.go @@ -67,7 +67,7 @@ func NewCmdDeleteCluster() *cobra.Command { return cmd } -// parseDeleteClusterCmd parses the command input into variables required to create a cluster +// parseDeleteClusterCmd parses the command input into variables required to delete clusters func parseDeleteClusterCmd(cmd *cobra.Command, args []string) (runtimes.Runtime, []*k3d.Cluster) { // --runtime rt, err := cmd.Flags().GetString("runtime") diff --git a/cmd/root.go b/cmd/root.go index 14bb08e4..282dc771 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -31,6 +31,8 @@ import ( "github.com/rancher/k3d/cmd/create" "github.com/rancher/k3d/cmd/delete" "github.com/rancher/k3d/cmd/get" + "github.com/rancher/k3d/cmd/start" + "github.com/rancher/k3d/cmd/stop" "github.com/rancher/k3d/version" @@ -79,6 +81,8 @@ func init() { rootCmd.AddCommand(create.NewCmdCreate()) rootCmd.AddCommand(delete.NewCmdDelete()) rootCmd.AddCommand(get.NewCmdGet()) + rootCmd.AddCommand(stop.NewCmdStop()) + rootCmd.AddCommand(start.NewCmdStart()) rootCmd.AddCommand(&cobra.Command{ Use: "version", diff --git a/cmd/start/startCluster.go b/cmd/start/startCluster.go index 9f64d90d..8de0abc9 100644 --- a/cmd/start/startCluster.go +++ b/cmd/start/startCluster.go @@ -22,9 +22,11 @@ THE SOFTWARE. package start import ( + "github.com/rancher/k3d/pkg/cluster" + "github.com/rancher/k3d/pkg/runtimes" "github.com/spf13/cobra" - "github.com/rancher/k3d/pkg/types" + k3d "github.com/rancher/k3d/pkg/types" log "github.com/sirupsen/logrus" ) @@ -39,14 +41,66 @@ func NewCmdStartCluster() *cobra.Command { Long: `Start an existing k3d cluster`, Run: func(cmd *cobra.Command, args []string) { log.Debugln("start cluster called") + runtime, clusters := parseStartClusterCmd(cmd, args) + if len(clusters) == 0 { + log.Infoln("No clusters found") + } else { + for _, c := range clusters { + if err := cluster.StartCluster(c, runtime); err != nil { + log.Fatalln(err) + } + } + } + + log.Debugln("...Finished") }, } // add flags - cmd.Flags().StringP("name", "n", types.DefaultClusterName, "Name of the cluster") + cmd.Flags().BoolP("all", "a", false, "Start all existing clusters") // add subcommands // done return cmd } + +// parseStartClusterCmd parses the command input into variables required to start clusters +func parseStartClusterCmd(cmd *cobra.Command, args []string) (runtimes.Runtime, []*k3d.Cluster) { + // --runtime + rt, err := cmd.Flags().GetString("runtime") + if err != nil { + log.Fatalln("No runtime specified") + } + runtime, err := runtimes.GetRuntime(rt) + if err != nil { + log.Fatalln(err) + } + + // --all + var clusters []*k3d.Cluster + + if all, err := cmd.Flags().GetBool("all"); err != nil { + log.Fatalln(err) + } else if all { + clusters, err = cluster.GetClusters(runtime) + if err != nil { + log.Fatalln(err) + } + return runtime, clusters + } + + if len(args) < 1 { + log.Fatalln("Expecting at least one cluster name if `--all` is not set") + } + + for _, name := range args { + cluster, err := cluster.GetCluster(&k3d.Cluster{Name: name}, runtime) + if err != nil { + log.Fatalln(err) + } + clusters = append(clusters, cluster) + } + + return runtime, clusters +} diff --git a/cmd/start/startNode.go b/cmd/start/startNode.go index 458478d2..62d01dd6 100644 --- a/cmd/start/startNode.go +++ b/cmd/start/startNode.go @@ -22,6 +22,8 @@ THE SOFTWARE. package start import ( + "github.com/rancher/k3d/pkg/runtimes" + k3d "github.com/rancher/k3d/pkg/types" "github.com/spf13/cobra" log "github.com/sirupsen/logrus" @@ -37,9 +39,33 @@ func NewCmdStartNode() *cobra.Command { Long: `Start an existing k3d node.`, Run: func(cmd *cobra.Command, args []string) { log.Debugln("start node called") + runtime, node := parseStartNodeCmd(cmd, args) + if err := runtime.StartNode(node); err != nil { + log.Fatalln(err) + } }, } // done return cmd } + +// parseStartNodeCmd parses the command input into variables required to start a node +func parseStartNodeCmd(cmd *cobra.Command, args []string) (runtimes.Runtime, *k3d.Node) { + // --runtime + rt, err := cmd.Flags().GetString("runtime") + if err != nil { + log.Fatalln("No runtime specified") + } + runtime, err := runtimes.GetRuntime(rt) + if err != nil { + log.Fatalln(err) + } + + // node name // TODO: allow node filters, e.g. `k3d start nodes mycluster@worker` to start all worker nodes of cluster 'mycluster' + if len(args) == 0 || len(args[0]) == 0 { + log.Fatalln("No node name given") + } + + return runtime, &k3d.Node{Name: args[0]} // TODO: validate and allow for more than one +} diff --git a/cmd/stop/stopCluster.go b/cmd/stop/stopCluster.go index 31e2444d..d5242e70 100644 --- a/cmd/stop/stopCluster.go +++ b/cmd/stop/stopCluster.go @@ -24,7 +24,9 @@ package stop import ( "github.com/spf13/cobra" - "github.com/rancher/k3d/pkg/types" + "github.com/rancher/k3d/pkg/cluster" + "github.com/rancher/k3d/pkg/runtimes" + k3d "github.com/rancher/k3d/pkg/types" log "github.com/sirupsen/logrus" ) @@ -39,14 +41,66 @@ func NewCmdStopCluster() *cobra.Command { Long: `Stop an existing k3d cluster.`, Run: func(cmd *cobra.Command, args []string) { log.Debugln("stop cluster called") + runtime, clusters := parseStopClusterCmd(cmd, args) + if len(clusters) == 0 { + log.Infoln("No clusters found") + } else { + for _, c := range clusters { + if err := cluster.StopCluster(c, runtime); err != nil { + log.Fatalln(err) + } + } + } + + log.Debugln("...Finished") }, } // add flags - cmd.Flags().StringP("name", "n", types.DefaultClusterName, "Name of the cluster") + cmd.Flags().BoolP("all", "a", false, "Start all existing clusters") // add subcommands // done return cmd } + +// parseStopClusterCmd parses the command input into variables required to start clusters +func parseStopClusterCmd(cmd *cobra.Command, args []string) (runtimes.Runtime, []*k3d.Cluster) { + // --runtime + rt, err := cmd.Flags().GetString("runtime") + if err != nil { + log.Fatalln("No runtime specified") + } + runtime, err := runtimes.GetRuntime(rt) + if err != nil { + log.Fatalln(err) + } + + // --all + var clusters []*k3d.Cluster + + if all, err := cmd.Flags().GetBool("all"); err != nil { + log.Fatalln(err) + } else if all { + clusters, err = cluster.GetClusters(runtime) + if err != nil { + log.Fatalln(err) + } + return runtime, clusters + } + + if len(args) < 1 { + log.Fatalln("Expecting at least one cluster name if `--all` is not set") + } + + for _, name := range args { + cluster, err := cluster.GetCluster(&k3d.Cluster{Name: name}, runtime) + if err != nil { + log.Fatalln(err) + } + clusters = append(clusters, cluster) + } + + return runtime, clusters +} diff --git a/cmd/stop/stopNode.go b/cmd/stop/stopNode.go index a7bf503e..94037f6f 100644 --- a/cmd/stop/stopNode.go +++ b/cmd/stop/stopNode.go @@ -22,8 +22,11 @@ THE SOFTWARE. package stop import ( + "github.com/rancher/k3d/pkg/runtimes" "github.com/spf13/cobra" + k3d "github.com/rancher/k3d/pkg/types" + log "github.com/sirupsen/logrus" ) @@ -37,9 +40,33 @@ func NewCmdStopNode() *cobra.Command { Long: `Stop an existing k3d node.`, Run: func(cmd *cobra.Command, args []string) { log.Debugln("stop node called") + runtime, node := parseStopNodeCmd(cmd, args) + if err := runtime.StopNode(node); err != nil { + log.Fatalln(err) + } }, } // done return cmd } + +// parseStopNodeCmd parses the command input into variables required to stop a node +func parseStopNodeCmd(cmd *cobra.Command, args []string) (runtimes.Runtime, *k3d.Node) { + // --runtime + rt, err := cmd.Flags().GetString("runtime") + if err != nil { + log.Fatalln("No runtime specified") + } + runtime, err := runtimes.GetRuntime(rt) + if err != nil { + log.Fatalln(err) + } + + // node name // TODO: allow node filters, e.g. `k3d stop nodes mycluster@worker` to stop all worker nodes of cluster 'mycluster' + if len(args) == 0 || len(args[0]) == 0 { + log.Fatalln("No node name given") + } + + return runtime, &k3d.Node{Name: args[0]} // TODO: validate and allow for more than one +} diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 45e48ac4..0f81533e 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -214,3 +214,41 @@ func GenerateClusterSecret() string { func generateNodeName(cluster string, role k3d.Role, suffix int) string { return fmt.Sprintf("%s-%s-%s-%d", k3d.DefaultObjectNamePrefix, cluster, role, suffix) } + +// StartCluster starts a whole cluster (i.e. all nodes of the cluster) +func StartCluster(cluster *k3d.Cluster, runtime k3drt.Runtime) error { + log.Infof("Starting cluster '%s'", cluster.Name) + + failed := 0 + for _, node := range cluster.Nodes { + if err := runtime.StartNode(node); err != nil { + log.Warningf("Failed to start node '%s': Try to start it manually", node.Name) + failed++ + continue + } + } + + if failed > 0 { + return fmt.Errorf("Failed to start %d nodes: Try to start them manually", failed) + } + return nil +} + +// StopCluster stops a whole cluster (i.e. all nodes of the cluster) +func StopCluster(cluster *k3d.Cluster, runtime k3drt.Runtime) error { + log.Infof("Stopping cluster '%s'", cluster.Name) + + failed := 0 + for _, node := range cluster.Nodes { + if err := runtime.StopNode(node); err != nil { + log.Warningf("Failed to stop node '%s': Try to stop it manually", node.Name) + failed++ + continue + } + } + + if failed > 0 { + return fmt.Errorf("Failed to stop %d nodes: Try to stop them manually", failed) + } + return nil +} diff --git a/pkg/runtimes/containerd/container.go b/pkg/runtimes/containerd/container.go index 71ceedee..fa28f66f 100644 --- a/pkg/runtimes/containerd/container.go +++ b/pkg/runtimes/containerd/container.go @@ -21,86 +21,3 @@ THE SOFTWARE. */ package containerd - -import ( - "context" - - "github.com/containerd/containerd" - "github.com/containerd/containerd/containers" - k3d "github.com/rancher/k3d/pkg/types" - log "github.com/sirupsen/logrus" -) - -// CreateNode creates a new k3d node -func (d Containerd) CreateNode(node *k3d.Node) error { - log.Debugln("containerd.CreateNode...") - - // create containerd client - ctx := context.Background() - clientOpts := []containerd.ClientOpt{ - containerd.WithDefaultNamespace("k3d"), - } - client, err := containerd.New("/run/containerd/containerd.sock", clientOpts...) // TODO: this is the default address on UNIX, different on Windows - if err != nil { - log.Errorln("Failed to create containerd client") - return err - } - - // create container - newContainerOpts := []containerd.NewContainerOpts{ - func(ctx context.Context, _ *containerd.Client, c *containers.Container) error { - c.Image = node.Image - c.Labels = node.Labels - return nil - }, - } - container, err := client.NewContainer(ctx, node.Name, newContainerOpts...) - if err != nil { - log.Errorln("Couldn't create container") - return err - } - - /* - // start container - task, err := container.NewTask(ctx, cio.NewCreator()) // TODO: how the hell does this work? - if err != nil { - log.Errorln("Failed to create task in container", container.ID) - return err - } - - task.Start(ctx) - */ - - log.Infoln("Created container with ID", container.ID()) - return nil -} - -// DeleteNode deletes an existing k3d node -func (d Containerd) DeleteNode(node *k3d.Node) error { - log.Debugln("containerd.DeleteNode...") - ctx := context.Background() - clientOpts := []containerd.ClientOpt{ - containerd.WithDefaultNamespace("k3d"), - } - client, err := containerd.New("/run/containerd/containerd.sock", clientOpts...) // TODO: this is the default address on UNIX, different on Windows - if err != nil { - log.Errorln("Failed to create containerd client") - return err - } - - container, err := client.LoadContainer(ctx, node.Name) - if err != nil { - log.Errorln("Couldn't load container", node.Name) - return err - } - if err = container.Delete(ctx, []containerd.DeleteOpts{}...); err != nil { - log.Errorln("Failed to delete container", container.ID) - return err - } - - return nil -} - -func (d Containerd) GetNodesByLabel(labels map[string]string) ([]*k3d.Node, error) { - return nil, nil -} diff --git a/pkg/runtimes/containerd/node.go b/pkg/runtimes/containerd/node.go new file mode 100644 index 00000000..c5dcba29 --- /dev/null +++ b/pkg/runtimes/containerd/node.go @@ -0,0 +1,116 @@ +/* +Copyright © 2019 Thorsten Klein + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package containerd + +import ( + "context" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/containers" + k3d "github.com/rancher/k3d/pkg/types" + log "github.com/sirupsen/logrus" +) + +// CreateNode creates a new k3d node +func (d Containerd) CreateNode(node *k3d.Node) error { + log.Debugln("containerd.CreateNode...") + + // create containerd client + ctx := context.Background() + clientOpts := []containerd.ClientOpt{ + containerd.WithDefaultNamespace("k3d"), + } + client, err := containerd.New("/run/containerd/containerd.sock", clientOpts...) // TODO: this is the default address on UNIX, different on Windows + if err != nil { + log.Errorln("Failed to create containerd client") + return err + } + + // create container + newContainerOpts := []containerd.NewContainerOpts{ + func(ctx context.Context, _ *containerd.Client, c *containers.Container) error { + c.Image = node.Image + c.Labels = node.Labels + return nil + }, + } + container, err := client.NewContainer(ctx, node.Name, newContainerOpts...) + if err != nil { + log.Errorln("Couldn't create container") + return err + } + + /* + // start container + task, err := container.NewTask(ctx, cio.NewCreator()) // TODO: how the hell does this work? + if err != nil { + log.Errorln("Failed to create task in container", container.ID) + return err + } + + task.Start(ctx) + */ + + log.Infoln("Created container with ID", container.ID()) + return nil +} + +// DeleteNode deletes an existing k3d node +func (d Containerd) DeleteNode(node *k3d.Node) error { + log.Debugln("containerd.DeleteNode...") + ctx := context.Background() + clientOpts := []containerd.ClientOpt{ + containerd.WithDefaultNamespace("k3d"), + } + client, err := containerd.New("/run/containerd/containerd.sock", clientOpts...) // TODO: this is the default address on UNIX, different on Windows + if err != nil { + log.Errorln("Failed to create containerd client") + return err + } + + container, err := client.LoadContainer(ctx, node.Name) + if err != nil { + log.Errorln("Couldn't load container", node.Name) + return err + } + if err = container.Delete(ctx, []containerd.DeleteOpts{}...); err != nil { + log.Errorln("Failed to delete container", container.ID) + return err + } + + return nil +} + +// StartNode starts an existing node +func (d Containerd) StartNode(node *k3d.Node) error { + return nil // TODO: fill +} + +// StopNode stops an existing node +func (d Containerd) StopNode(node *k3d.Node) error { + return nil // TODO: fill +} + +func (d Containerd) GetNodesByLabel(labels map[string]string) ([]*k3d.Node, error) { + return nil, nil +} diff --git a/pkg/runtimes/docker/container.go b/pkg/runtimes/docker/container.go index 3297bd90..0cb4a347 100644 --- a/pkg/runtimes/docker/container.go +++ b/pkg/runtimes/docker/container.go @@ -147,6 +147,7 @@ func getNodeContainer(node *k3d.Node) (*types.Container, error) { containers, err := docker.ContainerList(ctx, types.ContainerListOptions{ Filters: filters, + All: true, }) if err != nil { log.Errorln("Failed to list containers") @@ -158,6 +159,10 @@ func getNodeContainer(node *k3d.Node) (*types.Container, error) { return nil, err } + if len(containers) == 0 { + return nil, fmt.Errorf("Didn't find container for node '%s'", node.Name) + } + return &containers[0], nil } diff --git a/pkg/runtimes/docker/node.go b/pkg/runtimes/docker/node.go index b8c2bbad..da91653c 100644 --- a/pkg/runtimes/docker/node.go +++ b/pkg/runtimes/docker/node.go @@ -82,6 +82,64 @@ func (d Docker) GetNodesByLabel(labels map[string]string) ([]*k3d.Node, error) { } +// StartNode starts an existing node +func (d Docker) StartNode(node *k3d.Node) error { + // (0) create docker client + ctx := context.Background() + docker, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return fmt.Errorf("Failed to create docker client. %+v", err) + } + + // get container which represents the node + nodeContainer, err := getNodeContainer(node) + if err != nil { + log.Errorf("Failed to get container for node '%s'", node.Name) + return err + } + + // check if the container is actually managed by + if v, ok := nodeContainer.Labels["app"]; !ok || v != "k3d" { + return fmt.Errorf("Failed to determine if container '%s' is managed by k3d (needs label 'app=k3d')", nodeContainer.ID) + } + + // actually start the container + if err := docker.ContainerStart(ctx, nodeContainer.ID, types.ContainerStartOptions{}); err != nil { + return err + } + + return nil +} + +// StopNode stops an existing node +func (d Docker) StopNode(node *k3d.Node) error { + // (0) create docker client + ctx := context.Background() + docker, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return fmt.Errorf("Failed to create docker client. %+v", err) + } + + // get container which represents the node + nodeContainer, err := getNodeContainer(node) + if err != nil { + log.Errorf("Failed to get container for node '%s'", node.Name) + return err + } + + // check if the container is actually managed by + if v, ok := nodeContainer.Labels["app"]; !ok || v != "k3d" { + return fmt.Errorf("Failed to determine if container '%s' is managed by k3d (needs label 'app=k3d')", nodeContainer.ID) + } + + // actually stop the container + if err := docker.ContainerStop(ctx, nodeContainer.ID, nil); err != nil { + return err + } + + return nil +} + func getContainersByLabel(labels map[string]string) ([]types.Container, error) { // (0) create docker client ctx := context.Background() diff --git a/pkg/runtimes/runtime.go b/pkg/runtimes/runtime.go index b1a534fe..c0b93f00 100644 --- a/pkg/runtimes/runtime.go +++ b/pkg/runtimes/runtime.go @@ -44,9 +44,9 @@ type Runtime interface { CreateNetworkIfNotPresent(name string) (string, bool, error) // @return NETWORK_NAME, EXISTS, ERROR GetKubeconfig(*k3d.Node) (io.ReadCloser, error) DeleteNetwork(ID string) error - // StartContainer() error + StartNode(*k3d.Node) error + StopNode(*k3d.Node) error // ExecContainer() error - // StopContainer() error // DeleteContainer() error // GetContainerLogs() error }