diff --git a/cmd/node/node.go b/cmd/node/node.go new file mode 100644 index 00000000..a535a528 --- /dev/null +++ b/cmd/node/node.go @@ -0,0 +1,56 @@ +/* +Copyright © 2020 The k3d Author(s) + +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 node + +import ( + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +// NewCmdNode returns a new cobra command +func NewCmdNode() *cobra.Command { + + // create new cobra command + cmd := &cobra.Command{ + Use: "node", + Short: "Manage node(s)", + Long: `Manage node(s)`, + Run: func(cmd *cobra.Command, args []string) { + if err := cmd.Help(); err != nil { + log.Errorln("Couldn't get help text") + log.Fatalln(err) + } + }, + } + + // add subcommands + cmd.AddCommand(NewCmdNodeCreate()) + cmd.AddCommand(NewCmdNodeStart()) + cmd.AddCommand(NewCmdNodeStop()) + cmd.AddCommand(NewCmdNodeDelete()) + cmd.AddCommand(NewCmdNodeList()) + + // add flags + + // done + return cmd +} diff --git a/cmd/node/nodeCreate.go b/cmd/node/nodeCreate.go new file mode 100644 index 00000000..6356a5d5 --- /dev/null +++ b/cmd/node/nodeCreate.go @@ -0,0 +1,133 @@ +/* +Copyright © 2020 The k3d Author(s) + +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 node + +import ( + "fmt" + "time" + + "github.com/spf13/cobra" + + "github.com/rancher/k3d/v3/cmd/util" + k3dc "github.com/rancher/k3d/v3/pkg/cluster" + "github.com/rancher/k3d/v3/pkg/runtimes" + k3d "github.com/rancher/k3d/v3/pkg/types" + "github.com/rancher/k3d/v3/version" + log "github.com/sirupsen/logrus" +) + +// NewCmdNodeCreate returns a new cobra command +func NewCmdNodeCreate() *cobra.Command { + + createNodeOpts := k3d.CreateNodeOpts{} + + // create new command + cmd := &cobra.Command{ + Use: "create NAME", + Short: "Create a new k3s node in docker", + Long: `Create a new containerized k3s node (k3s in docker).`, + Args: cobra.ExactArgs(1), // exactly one name accepted // TODO: if not specified, inherit from cluster that the node shall belong to, if that is specified + Run: func(cmd *cobra.Command, args []string) { + nodes, cluster := parseCreateNodeCmd(cmd, args) + if err := k3dc.AddNodesToCluster(cmd.Context(), runtimes.SelectedRuntime, nodes, cluster, createNodeOpts); err != nil { + log.Errorf("Failed to add nodes to cluster '%s'", cluster.Name) + log.Errorln(err) + } + }, + } + + // add flags + cmd.Flags().Int("replicas", 1, "Number of replicas of this node specification.") + cmd.Flags().String("role", string(k3d.WorkerRole), "Specify node role [master, worker]") + if err := cmd.RegisterFlagCompletionFunc("role", util.ValidArgsNodeRoles); err != nil { + log.Fatalln("Failed to register flag completion for '--role'", err) + } + cmd.Flags().StringP("cluster", "c", k3d.DefaultClusterName, "Select the cluster that the node shall connect to.") + if err := cmd.MarkFlagRequired("cluster"); err != nil { + log.Fatalln("Failed to mark required flag '--cluster'") + } + if err := cmd.RegisterFlagCompletionFunc("cluster", util.ValidArgsAvailableClusters); err != nil { + log.Fatalln("Failed to register flag completion for '--cluster'", err) + } + + cmd.Flags().StringP("image", "i", fmt.Sprintf("%s:%s", k3d.DefaultK3sImageRepo, version.GetK3sVersion(false)), "Specify k3s image used for the node(s)") + + cmd.Flags().BoolVar(&createNodeOpts.Wait, "wait", false, "Wait for the node(s) to be ready before returning.") + cmd.Flags().DurationVar(&createNodeOpts.Timeout, "timeout", 0*time.Second, "Maximum waiting time for '--wait' before canceling/returning.") + + // done + return cmd +} + +// parseCreateNodeCmd parses the command input into variables required to create a cluster +func parseCreateNodeCmd(cmd *cobra.Command, args []string) ([]*k3d.Node, *k3d.Cluster) { + + // --replicas + replicas, err := cmd.Flags().GetInt("replicas") + if err != nil { + log.Errorln("No replica count specified") + log.Fatalln(err) + } + + // --role + roleStr, err := cmd.Flags().GetString("role") + if err != nil { + log.Errorln("No node role specified") + log.Fatalln(err) + } + if _, ok := k3d.NodeRoles[roleStr]; !ok { + log.Fatalf("Unknown node role '%s'\n", roleStr) + } + role := k3d.NodeRoles[roleStr] + + // --image + image, err := cmd.Flags().GetString("image") + if err != nil { + log.Errorln("No image specified") + log.Fatalln(err) + } + + // --cluster + clusterName, err := cmd.Flags().GetString("cluster") + if err != nil { + log.Fatalln(err) + } + cluster := &k3d.Cluster{ + Name: clusterName, + } + + // generate list of nodes + nodes := []*k3d.Node{} + for i := 0; i < replicas; i++ { + node := &k3d.Node{ + Name: fmt.Sprintf("%s-%s-%d", k3d.DefaultObjectNamePrefix, args[0], i), + Role: role, + Image: image, + Labels: map[string]string{ + k3d.LabelRole: roleStr, + }, + } + nodes = append(nodes, node) + } + + return nodes, cluster +} diff --git a/cmd/node/nodeDelete.go b/cmd/node/nodeDelete.go new file mode 100644 index 00000000..3805a419 --- /dev/null +++ b/cmd/node/nodeDelete.go @@ -0,0 +1,97 @@ +/* +Copyright © 2020 The k3d Author(s) + +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 node + +import ( + "github.com/rancher/k3d/v3/cmd/util" + "github.com/rancher/k3d/v3/pkg/cluster" + "github.com/rancher/k3d/v3/pkg/runtimes" + k3d "github.com/rancher/k3d/v3/pkg/types" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +// NewCmdNodeDelete returns a new cobra command +func NewCmdNodeDelete() *cobra.Command { + + // create new cobra command + cmd := &cobra.Command{ + Use: "delete (NAME | --all)", + Short: "Delete node(s).", + Long: `Delete node(s).`, + Args: cobra.MinimumNArgs(1), // at least one node has to be specified + ValidArgsFunction: util.ValidArgsAvailableNodes, + Run: func(cmd *cobra.Command, args []string) { + + nodes := parseDeleteNodeCmd(cmd, args) + + if len(nodes) == 0 { + log.Infoln("No nodes found") + } else { + for _, node := range nodes { + if err := cluster.DeleteNode(cmd.Context(), runtimes.SelectedRuntime, node); err != nil { + log.Fatalln(err) + } + } + } + }, + } + + // add subcommands + + // add flags + cmd.Flags().BoolP("all", "a", false, "Delete all existing clusters") + + // done + return cmd +} + +// parseDeleteNodeCmd parses the command input into variables required to delete nodes +func parseDeleteNodeCmd(cmd *cobra.Command, args []string) []*k3d.Node { + + // --all + var nodes []*k3d.Node + + if all, err := cmd.Flags().GetBool("all"); err != nil { + log.Fatalln(err) + } else if all { + nodes, err = cluster.GetNodes(cmd.Context(), runtimes.SelectedRuntime) + if err != nil { + log.Fatalln(err) + } + return nodes + } + + if len(args) < 1 { + log.Fatalln("Expecting at least one node name if `--all` is not set") + } + + for _, name := range args { + node, err := cluster.GetNode(cmd.Context(), runtimes.SelectedRuntime, &k3d.Node{Name: name}) + if err != nil { + log.Fatalln(err) + } + nodes = append(nodes, node) + } + + return nodes +} diff --git a/cmd/node/nodeList.go b/cmd/node/nodeList.go new file mode 100644 index 00000000..18afae6a --- /dev/null +++ b/cmd/node/nodeList.go @@ -0,0 +1,123 @@ +/* +Copyright © 2020 The k3d Author(s) + +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 node + +import ( + "fmt" + "os" + "sort" + "strings" + + "github.com/liggitt/tabwriter" + "github.com/rancher/k3d/v3/cmd/util" + "github.com/rancher/k3d/v3/pkg/cluster" + "github.com/rancher/k3d/v3/pkg/runtimes" + k3d "github.com/rancher/k3d/v3/pkg/types" + "github.com/spf13/cobra" + + log "github.com/sirupsen/logrus" +) + +// NewCmdNodeList returns a new cobra command +func NewCmdNodeList() *cobra.Command { + + // create new command + cmd := &cobra.Command{ + Use: "list [NAME [NAME...]]", + Aliases: []string{"ls"}, + Short: "List node(s)", + Long: `List node(s).`, + Args: cobra.MinimumNArgs(0), // 0 or more; 0 = all + ValidArgsFunction: util.ValidArgsAvailableNodes, + Run: func(cmd *cobra.Command, args []string) { + nodes, headersOff := parseGetNodeCmd(cmd, args) + var existingNodes []*k3d.Node + if len(nodes) == 0 { // Option a) no name specified -> get all nodes + found, err := cluster.GetNodes(cmd.Context(), runtimes.SelectedRuntime) + if err != nil { + log.Fatalln(err) + } + existingNodes = append(existingNodes, found...) + } else { // Option b) cluster name specified -> get specific cluster + for _, node := range nodes { + found, err := cluster.GetNode(cmd.Context(), runtimes.SelectedRuntime, node) + if err != nil { + log.Fatalln(err) + } + existingNodes = append(existingNodes, found) + } + } + // print existing clusters + printNodes(existingNodes, headersOff) + }, + } + + // add flags + cmd.Flags().Bool("no-headers", false, "Disable headers") + + // add subcommands + + // done + return cmd +} + +func parseGetNodeCmd(cmd *cobra.Command, args []string) ([]*k3d.Node, bool) { + // --no-headers + headersOff, err := cmd.Flags().GetBool("no-headers") + if err != nil { + log.Fatalln(err) + } + + // Args = node name + if len(args) == 0 { + return nil, headersOff + } + + nodes := []*k3d.Node{} + for _, name := range args { + nodes = append(nodes, &k3d.Node{Name: name}) + } + + return nodes, headersOff +} + +func printNodes(nodes []*k3d.Node, headersOff bool) { + + tabwriter := tabwriter.NewWriter(os.Stdout, 6, 4, 3, ' ', tabwriter.RememberWidths) + defer tabwriter.Flush() + + if !headersOff { + headers := []string{"NAME", "ROLE", "CLUSTER"} // TODO: add status + _, err := fmt.Fprintf(tabwriter, "%s\n", strings.Join(headers, "\t")) + if err != nil { + log.Fatalln("Failed to print headers") + } + } + + sort.Slice(nodes, func(i, j int) bool { + return nodes[i].Name < nodes[j].Name + }) + + for _, node := range nodes { + fmt.Fprintf(tabwriter, "%s\t%s\t%s\n", strings.TrimPrefix(node.Name, "/"), string(node.Role), node.Labels[k3d.LabelClusterName]) + } +} diff --git a/cmd/node/nodeStart.go b/cmd/node/nodeStart.go new file mode 100644 index 00000000..acd4530e --- /dev/null +++ b/cmd/node/nodeStart.go @@ -0,0 +1,62 @@ +/* +Copyright © 2020 The k3d Author(s) + +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 node + +import ( + "github.com/rancher/k3d/v3/cmd/util" + "github.com/rancher/k3d/v3/pkg/runtimes" + k3d "github.com/rancher/k3d/v3/pkg/types" + "github.com/spf13/cobra" + + log "github.com/sirupsen/logrus" +) + +// NewCmdNodeStart returns a new cobra command +func NewCmdNodeStart() *cobra.Command { + + // create new command + cmd := &cobra.Command{ + Use: "start NAME", // TODO: startNode: allow one or more names or --all + Short: "Start an existing k3d node", + Long: `Start an existing k3d node.`, + ValidArgsFunction: util.ValidArgsAvailableNodes, + Run: func(cmd *cobra.Command, args []string) { + node := parseStartNodeCmd(cmd, args) + if err := runtimes.SelectedRuntime.StartNode(cmd.Context(), 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) *k3d.Node { + // node name // TODO: startNode: 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 &k3d.Node{Name: args[0]} +} diff --git a/cmd/node/nodeStop.go b/cmd/node/nodeStop.go new file mode 100644 index 00000000..cc2a2133 --- /dev/null +++ b/cmd/node/nodeStop.go @@ -0,0 +1,63 @@ +/* +Copyright © 2020 The k3d Author(s) + +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 node + +import ( + "github.com/rancher/k3d/v3/cmd/util" + "github.com/rancher/k3d/v3/pkg/runtimes" + "github.com/spf13/cobra" + + k3d "github.com/rancher/k3d/v3/pkg/types" + + log "github.com/sirupsen/logrus" +) + +// NewCmdNodeStop returns a new cobra command +func NewCmdNodeStop() *cobra.Command { + + // create new command + cmd := &cobra.Command{ + Use: "stop NAME", // TODO: stopNode: allow one or more names or --all", + Short: "Stop an existing k3d node", + Long: `Stop an existing k3d node.`, + ValidArgsFunction: util.ValidArgsAvailableNodes, + Run: func(cmd *cobra.Command, args []string) { + node := parseStopNodeCmd(cmd, args) + if err := runtimes.SelectedRuntime.StopNode(cmd.Context(), 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) *k3d.Node { + // 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 &k3d.Node{Name: args[0]} +} diff --git a/cmd/root.go b/cmd/root.go index d24caf41..3a0e4c37 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -36,6 +36,7 @@ import ( "github.com/rancher/k3d/v3/cmd/get" "github.com/rancher/k3d/v3/cmd/kubeconfig" "github.com/rancher/k3d/v3/cmd/load" + "github.com/rancher/k3d/v3/cmd/node" "github.com/rancher/k3d/v3/cmd/start" "github.com/rancher/k3d/v3/cmd/stop" "github.com/rancher/k3d/v3/pkg/runtimes" @@ -104,6 +105,7 @@ func init() { if os.Getenv("K3D_NEW_SYNTAX") == "1" { rootCmd.AddCommand(cluster.NewCmdCluster()) rootCmd.AddCommand(kubeconfig.NewCmdKubeconfig()) + rootCmd.AddCommand(node.NewCmdNode()) } rootCmd.AddCommand(&cobra.Command{