diff --git a/cmd/create/createCluster.go b/cmd/create/createCluster.go index 74ae44ef..ff151de8 100644 --- a/cmd/create/createCluster.go +++ b/cmd/create/createCluster.go @@ -25,6 +25,7 @@ package create import ( "fmt" "os" + "time" "github.com/spf13/cobra" @@ -50,11 +51,17 @@ func NewCmdCreateCluster() *cobra.Command { Long: `Create a new k3s cluster with containerized nodes (k3s in docker).`, Args: cobra.RangeArgs(0, 1), // exactly one cluster name can be set (default: k3d.DefaultClusterName) Run: func(cmd *cobra.Command, args []string) { + // parse args and flags cluster := parseCreateClusterCmd(cmd, args, createClusterOpts) + + // check if a cluster with that name exists already if _, err := k3dCluster.GetCluster(cluster, runtimes.SelectedRuntime); err == nil { log.Fatalf("Failed to create cluster '%s' because a cluster with that name already exists", cluster.Name) } + + // create cluster if err := k3dCluster.CreateCluster(cluster, runtimes.SelectedRuntime); err != nil { + // rollback if creation failed log.Errorln(err) log.Errorln("Failed to create cluster >>> Rolling Back") if err := k3dCluster.DeleteCluster(cluster, runtimes.SelectedRuntime); err != nil { @@ -63,6 +70,8 @@ func NewCmdCreateCluster() *cobra.Command { } log.Fatalln("Cluster creation FAILED, all changes have been rolled back!") } + + // print information on how to use the cluster with kubectl log.Infof("Cluster '%s' created successfully. You can now use it like this:", cluster.Name) fmt.Printf("export KUBECONFIG=$(%s get kubeconfig %s)\n", os.Args[0], cluster.Name) fmt.Println("kubectl cluster-info") @@ -80,7 +89,8 @@ func NewCmdCreateCluster() *cobra.Command { cmd.Flags().String("secret", "", "Specify a cluster secret. By default, we generate one.") cmd.Flags().StringArrayP("volume", "v", nil, "Mount volumes into the nodes (Format: `--volume [SOURCE:]DEST[@NODEFILTER[;NODEFILTER...]]`\n - Example: `k3d create -w 2 -v /my/path@worker[0,1] -v /tmp/test:/tmp/other@master[0]`") cmd.Flags().StringArrayP("port", "p", nil, "Map ports from the node containers to the host (Format: `[HOST:][HOSTPORT:]CONTAINERPORT[/PROTOCOL][@NODEFILTER]`)\n - Example: `k3d create -w 2 -p 8080:80@worker[0] -p 8081@worker[1]`") - cmd.Flags().IntVar(&createClusterOpts.WaitForMaster, "wait", -1, "Wait for a specified amount of time (seconds >= 0, where 0 means forever) for the master(s) to be ready or timeout and rollback before returning") + cmd.Flags().BoolVar(&createClusterOpts.WaitForMaster, "wait", false, "Wait for for the master(s) to be ready before returning. Use `--timeout DURATION` to not wait forever.") + cmd.Flags().DurationVar(&createClusterOpts.Timeout, "timeout", 0*time.Second, "Rollback changes if cluster couldn't be created in specified duration.") /* Image Importing */ cmd.Flags().BoolVar(&createClusterOpts.DisableImageVolume, "no-image-volume", false, "Disable the creation of a volume for importing images") @@ -167,9 +177,9 @@ func parseCreateClusterCmd(cmd *cobra.Command, args []string, createClusterOpts log.Fatalln(err) } - // --wait - if cmd.Flags().Changed("wait") && createClusterOpts.WaitForMaster < 0 { - log.Fatalln("Value of '--wait' can't be less than 0") + // --timeout + if cmd.Flags().Changed("timeout") && createClusterOpts.Timeout <= 0*time.Second { + log.Fatalln("--timeout DURATION must be >= 1s") } // --api-port diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 28edf1ef..96705fc0 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -23,6 +23,7 @@ package cluster import ( "bytes" + "context" "fmt" "strconv" "strings" @@ -40,6 +41,11 @@ import ( // - a docker network func CreateCluster(cluster *k3d.Cluster, runtime k3drt.Runtime) error { + log.Debugf("Cluster creation will timeout after %s at %s", cluster.CreateClusterOpts.Timeout, time.Now().Add(cluster.CreateClusterOpts.Timeout)) + + ctx, cancel := context.WithTimeout(context.Background(), cluster.CreateClusterOpts.Timeout) + defer cancel() + /* * Network */ @@ -212,14 +218,14 @@ func CreateCluster(cluster *k3d.Cluster, runtime k3drt.Runtime) error { } // asynchronously wait for this master node to be ready (by checking the logs for a specific log mesage) - if node.Role == k3d.MasterRole && cluster.CreateClusterOpts.WaitForMaster >= 0 { + if node.Role == k3d.MasterRole && cluster.CreateClusterOpts.Timeout >= time.Second { waitForMasterWaitgroup.Add(1) go func(masterNode *k3d.Node) { // TODO: avoid `level=fatal msg="starting kubernetes: preparing server: post join: a configuration change is already in progress (5)"` // ... by scanning for this line in logs and restarting the container in case it appears log.Debugf("Starting to wait for master node '%s'", masterNode.Name) // TODO: it may be better to give endtime=starttime+timeout here so that there is no difference between the instances (go func may be called with a few (milli-)seconds difference) - err := WaitForNodeLogMessage(runtime, masterNode, "Wrote kubeconfig", (time.Duration(cluster.CreateClusterOpts.WaitForMaster) * time.Second)) + err := WaitForNodeLogMessage(runtime, masterNode, "Wrote kubeconfig", (time.Duration(cluster.CreateClusterOpts.Timeout) * time.Second)) waitForMasterErrChan <- err if err == nil { log.Debugf("Master Node '%s' ready", masterNode.Name) @@ -229,7 +235,7 @@ func CreateCluster(cluster *k3d.Cluster, runtime k3drt.Runtime) error { } // block until all masters are ready (if --wait was set) and collect errors if not - if cluster.CreateClusterOpts.WaitForMaster >= 0 { + if cluster.CreateClusterOpts.Timeout >= time.Second { errs := []error{} go func() { for elem := range waitForMasterErrChan { diff --git a/pkg/types/types.go b/pkg/types/types.go index ff5ab738..59b163b4 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -23,6 +23,7 @@ package types import ( "fmt" + "time" ) // DefaultClusterName specifies the default name used for newly created clusters @@ -97,7 +98,8 @@ const DefaultAPIHost = "0.0.0.0" // CreateClusterOpts describe a set of options one can set when creating a cluster type CreateClusterOpts struct { DisableImageVolume bool - WaitForMaster int + WaitForMaster bool + Timeout time.Duration K3sServerArgs []string K3sAgentArgs []string }