diff --git a/docs/src/_parts/commands/k8s_bootstrap.md b/docs/src/_parts/commands/k8s_bootstrap.md index 32fd8d89d..73bedab2f 100644 --- a/docs/src/_parts/commands/k8s_bootstrap.md +++ b/docs/src/_parts/commands/k8s_bootstrap.md @@ -19,6 +19,7 @@ k8s bootstrap [flags] --interactive interactively configure the most important cluster options --name string node name, defaults to hostname --output-format string set the output format to one of plain, json or yaml (default "plain") + --timeout duration the max time to wait for the command to execute (default 1m30s) ``` ### SEE ALSO diff --git a/docs/src/_parts/commands/k8s_disable.md b/docs/src/_parts/commands/k8s_disable.md index bc25c6e7d..97ebaef34 100644 --- a/docs/src/_parts/commands/k8s_disable.md +++ b/docs/src/_parts/commands/k8s_disable.md @@ -15,6 +15,7 @@ k8s disable ... [flags] ``` -h, --help help for disable --output-format string set the output format to one of plain, json or yaml (default "plain") + --timeout duration the max time to wait for the command to execute (default 1m30s) ``` ### SEE ALSO diff --git a/docs/src/_parts/commands/k8s_enable.md b/docs/src/_parts/commands/k8s_enable.md index 2fc499e44..cdb972a29 100644 --- a/docs/src/_parts/commands/k8s_enable.md +++ b/docs/src/_parts/commands/k8s_enable.md @@ -15,6 +15,7 @@ k8s enable ... [flags] ``` -h, --help help for enable --output-format string set the output format to one of plain, json or yaml (default "plain") + --timeout duration the max time to wait for the command to execute (default 1m30s) ``` ### SEE ALSO diff --git a/docs/src/_parts/commands/k8s_get-join-token.md b/docs/src/_parts/commands/k8s_get-join-token.md index bcf02583a..ca584fd85 100644 --- a/docs/src/_parts/commands/k8s_get-join-token.md +++ b/docs/src/_parts/commands/k8s_get-join-token.md @@ -9,8 +9,9 @@ k8s get-join-token [flags] ### Options ``` - -h, --help help for get-join-token - --worker generate a join token for a worker node + -h, --help help for get-join-token + --timeout duration the max time to wait for the command to execute (default 1m30s) + --worker generate a join token for a worker node ``` ### SEE ALSO diff --git a/docs/src/_parts/commands/k8s_get.md b/docs/src/_parts/commands/k8s_get.md index b02c3dc0c..f31646363 100644 --- a/docs/src/_parts/commands/k8s_get.md +++ b/docs/src/_parts/commands/k8s_get.md @@ -15,6 +15,7 @@ k8s get [flags] ``` -h, --help help for get --output-format string set the output format to one of plain, json or yaml (default "plain") + --timeout duration the max time to wait for the command to execute (default 1m30s) ``` ### SEE ALSO diff --git a/docs/src/_parts/commands/k8s_join-cluster.md b/docs/src/_parts/commands/k8s_join-cluster.md index 2d786398b..a1693f57a 100644 --- a/docs/src/_parts/commands/k8s_join-cluster.md +++ b/docs/src/_parts/commands/k8s_join-cluster.md @@ -14,6 +14,7 @@ k8s join-cluster [flags] -h, --help help for join-cluster --name string node name, defaults to hostname --output-format string set the output format to one of plain, json or yaml (default "plain") + --timeout duration the max time to wait for the command to execute (default 1m30s) ``` ### SEE ALSO diff --git a/docs/src/_parts/commands/k8s_remove-node.md b/docs/src/_parts/commands/k8s_remove-node.md index 59042cc4c..ac5bc3ed1 100644 --- a/docs/src/_parts/commands/k8s_remove-node.md +++ b/docs/src/_parts/commands/k8s_remove-node.md @@ -12,6 +12,7 @@ k8s remove-node [flags] --force forcibly remove the cluster member -h, --help help for remove-node --output-format string set the output format to one of plain, json or yaml (default "plain") + --timeout duration the max time to wait for the command to execute (default 1m30s) ``` ### SEE ALSO diff --git a/docs/src/_parts/commands/k8s_set.md b/docs/src/_parts/commands/k8s_set.md index e1402b047..2263abb38 100644 --- a/docs/src/_parts/commands/k8s_set.md +++ b/docs/src/_parts/commands/k8s_set.md @@ -16,6 +16,7 @@ k8s set ... [flags] ``` -h, --help help for set --output-format string set the output format to one of plain, json or yaml (default "plain") + --timeout duration the max time to wait for the command to execute (default 1m30s) ``` ### SEE ALSO diff --git a/docs/src/_parts/commands/k8s_status.md b/docs/src/_parts/commands/k8s_status.md index c40b82f13..c3e2789a5 100644 --- a/docs/src/_parts/commands/k8s_status.md +++ b/docs/src/_parts/commands/k8s_status.md @@ -11,6 +11,7 @@ k8s status [flags] ``` -h, --help help for status --output-format string set the output format to one of plain, json or yaml (default "plain") + --timeout duration the max time to wait for the command to execute (default 1m30s) --wait-ready wait until at least one cluster node is ready ``` diff --git a/src/k8s/cmd/k8s/k8s.go b/src/k8s/cmd/k8s/k8s.go index 297d9bd1c..8ff06ba36 100644 --- a/src/k8s/cmd/k8s/k8s.go +++ b/src/k8s/cmd/k8s/k8s.go @@ -1,7 +1,6 @@ package k8s import ( - "context" "time" cmdutil "github.com/canonical/k8s/cmd/util" @@ -14,6 +13,8 @@ var ( outputFormatter cmdutil.Formatter ) +const minTimeout = 3 * time.Second + func addCommands(root *cobra.Command, group *cobra.Group, commands ...*cobra.Command) { if group != nil { root.AddGroup(group) @@ -31,28 +32,11 @@ func NewRootCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { logDebug bool logVerbose bool stateDir string - timeout time.Duration } ) cmd := &cobra.Command{ Use: "k8s", Short: "Canonical Kubernetes CLI", - PersistentPreRun: func(cmd *cobra.Command, args []string) { - // initialize context - ctx := cmd.Context() - - // configure command context timeout - const minTimeout = 3 * time.Second - if opts.timeout < minTimeout { - cmd.PrintErrf("Timeout %v is less than minimum of %v. Using the minimum %v instead.\n", opts.timeout, minTimeout, minTimeout) - opts.timeout = minTimeout - } - - ctx, cancel := context.WithTimeout(ctx, opts.timeout) - cobra.OnFinalize(cancel) - - cmd.SetContext(ctx) - }, } // set input/output streams @@ -63,14 +47,12 @@ func NewRootCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { cmd.PersistentFlags().StringVar(&opts.stateDir, "state-dir", "", "directory with the dqlite datastore") cmd.PersistentFlags().BoolVarP(&opts.logDebug, "debug", "d", false, "show all debug messages") cmd.PersistentFlags().BoolVarP(&opts.logVerbose, "verbose", "v", true, "show all information messages") - cmd.PersistentFlags().DurationVar(&opts.timeout, "timeout", 90*time.Second, "the max time to wait for the command to execute") // By default, the state dir is set to a fixed directory in the snap. // This shouldn't be overwritten by the user. cmd.PersistentFlags().MarkHidden("state-dir") cmd.PersistentFlags().MarkHidden("debug") cmd.PersistentFlags().MarkHidden("verbose") - cmd.PersistentFlags().MarkHidden("timeout") // General addCommands( diff --git a/src/k8s/cmd/k8s/k8s_bootstrap.go b/src/k8s/cmd/k8s/k8s_bootstrap.go index 9fb338dbe..127d3b9cc 100644 --- a/src/k8s/cmd/k8s/k8s_bootstrap.go +++ b/src/k8s/cmd/k8s/k8s_bootstrap.go @@ -3,11 +3,13 @@ package k8s import ( "bufio" "bytes" + "context" "fmt" "io" "os" "slices" "strings" + "time" "unicode" apiv1 "github.com/canonical/k8s/api/v1" @@ -38,6 +40,7 @@ func newBootstrapCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { name string address string outputFormat string + timeout time.Duration } cmd := &cobra.Command{ Use: "bootstrap", @@ -51,6 +54,11 @@ func newBootstrapCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { return } + if opts.timeout < minTimeout { + cmd.PrintErrf("Timeout %v is less than minimum of %v. Using the minimum %v instead.\n", opts.timeout, minTimeout, minTimeout) + opts.timeout = minTimeout + } + // Use hostname as default node name if opts.name == "" { hostname, err := os.Hostname() @@ -122,7 +130,10 @@ func newBootstrapCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { Config: bootstrapConfig, } - node, err := client.Bootstrap(cmd.Context(), request) + ctx, cancel := context.WithTimeout(cmd.Context(), opts.timeout) + cobra.OnFinalize(cancel) + + node, err := client.Bootstrap(ctx, request) if err != nil { cmd.PrintErrf("Error: Failed to bootstrap the cluster.\n\nThe error was: %v\n", err) env.Exit(1) @@ -138,6 +149,7 @@ func newBootstrapCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { cmd.Flags().StringVar(&opts.name, "name", "", "node name, defaults to hostname") cmd.Flags().StringVar(&opts.address, "address", "", "microcluster address, defaults to the node IP address") cmd.Flags().StringVar(&opts.outputFormat, "output-format", "plain", "set the output format to one of plain, json or yaml") + cmd.Flags().DurationVar(&opts.timeout, "timeout", 90*time.Second, "the max time to wait for the command to execute") return cmd } diff --git a/src/k8s/cmd/k8s/k8s_config.go b/src/k8s/cmd/k8s/k8s_config.go index 2072fde12..5e9afdb01 100644 --- a/src/k8s/cmd/k8s/k8s_config.go +++ b/src/k8s/cmd/k8s/k8s_config.go @@ -1,6 +1,9 @@ package k8s import ( + "context" + "time" + apiv1 "github.com/canonical/k8s/api/v1" cmdutil "github.com/canonical/k8s/cmd/util" "github.com/spf13/cobra" @@ -8,7 +11,8 @@ import ( func newKubeConfigCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { var opts struct { - server string + server string + timeout time.Duration } cmd := &cobra.Command{ Use: "config", @@ -16,6 +20,11 @@ func newKubeConfigCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { Short: "Generate an admin kubeconfig that can be used to access the Kubernetes cluster", PreRun: chainPreRunHooks(hookRequireRoot(env)), Run: func(cmd *cobra.Command, args []string) { + if opts.timeout < minTimeout { + cmd.PrintErrf("Timeout %v is less than minimum of %v. Using the minimum %v instead.\n", opts.timeout, minTimeout, minTimeout) + opts.timeout = minTimeout + } + client, err := env.Client(cmd.Context()) if err != nil { cmd.PrintErrf("Error: Failed to create a k8sd client. Make sure that the k8sd service is running.\n\nThe error was: %v\n", err) @@ -29,7 +38,10 @@ func newKubeConfigCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { return } - config, err := client.KubeConfig(cmd.Context(), apiv1.GetKubeConfigRequest{Server: opts.server}) + ctx, cancel := context.WithTimeout(cmd.Context(), opts.timeout) + cobra.OnFinalize(cancel) + + config, err := client.KubeConfig(ctx, apiv1.GetKubeConfigRequest{Server: opts.server}) if err != nil { cmd.PrintErrf("Error: Failed to generate an admin kubeconfig for %q.\n\nThe error was: %v\n", opts.server, err) env.Exit(1) @@ -40,5 +52,6 @@ func newKubeConfigCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { }, } cmd.Flags().StringVar(&opts.server, "server", "", "custom cluster server address") + cmd.Flags().DurationVar(&opts.timeout, "timeout", 90*time.Second, "the max time to wait for the command to execute") return cmd } diff --git a/src/k8s/cmd/k8s/k8s_disable.go b/src/k8s/cmd/k8s/k8s_disable.go index 208b92bc2..698a3ef12 100644 --- a/src/k8s/cmd/k8s/k8s_disable.go +++ b/src/k8s/cmd/k8s/k8s_disable.go @@ -1,9 +1,12 @@ package k8s import ( + "context" "fmt" - "github.com/canonical/k8s/pkg/utils" "strings" + "time" + + "github.com/canonical/k8s/pkg/utils" api "github.com/canonical/k8s/api/v1" cmdutil "github.com/canonical/k8s/cmd/util" @@ -21,6 +24,7 @@ func (d DisableResult) String() string { func newDisableCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { var opts struct { outputFormat string + timeout time.Duration } cmd := &cobra.Command{ Use: "disable ...", @@ -31,6 +35,11 @@ func newDisableCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { Run: func(cmd *cobra.Command, args []string) { config := api.UserFacingClusterConfig{} + if opts.timeout < minTimeout { + cmd.PrintErrf("Timeout %v is less than minimum of %v. Using the minimum %v instead.\n", opts.timeout, minTimeout, minTimeout) + opts.timeout = minTimeout + } + for _, feature := range args { switch feature { case "network": @@ -79,7 +88,9 @@ func newDisableCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { } cmd.PrintErrf("Disabling %s from the cluster. This may take a few seconds, please wait.\n", strings.Join(args, ", ")) - if err := client.UpdateClusterConfig(cmd.Context(), request); err != nil { + ctx, cancel := context.WithTimeout(cmd.Context(), opts.timeout) + cobra.OnFinalize(cancel) + if err := client.UpdateClusterConfig(ctx, request); err != nil { cmd.PrintErrf("Error: Failed to disable %s from the cluster.\n\nThe error was: %v\n", strings.Join(args, ", "), err) env.Exit(1) return @@ -90,6 +101,7 @@ func newDisableCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { } cmd.Flags().StringVar(&opts.outputFormat, "output-format", "plain", "set the output format to one of plain, json or yaml") + cmd.Flags().DurationVar(&opts.timeout, "timeout", 90*time.Second, "the max time to wait for the command to execute") return cmd } diff --git a/src/k8s/cmd/k8s/k8s_enable.go b/src/k8s/cmd/k8s/k8s_enable.go index 0216d0314..091fbe992 100644 --- a/src/k8s/cmd/k8s/k8s_enable.go +++ b/src/k8s/cmd/k8s/k8s_enable.go @@ -1,9 +1,12 @@ package k8s import ( + "context" "fmt" - "github.com/canonical/k8s/pkg/utils" "strings" + "time" + + "github.com/canonical/k8s/pkg/utils" api "github.com/canonical/k8s/api/v1" cmdutil "github.com/canonical/k8s/cmd/util" @@ -21,6 +24,7 @@ func (e EnableResult) String() string { func newEnableCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { var opts struct { outputFormat string + timeout time.Duration } cmd := &cobra.Command{ Use: "enable ...", @@ -31,6 +35,11 @@ func newEnableCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { Run: func(cmd *cobra.Command, args []string) { config := api.UserFacingClusterConfig{} + if opts.timeout < minTimeout { + cmd.PrintErrf("Timeout %v is less than minimum of %v. Using the minimum %v instead.\n", opts.timeout, minTimeout, minTimeout) + opts.timeout = minTimeout + } + for _, feature := range args { switch feature { case "network": @@ -79,7 +88,9 @@ func newEnableCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { } cmd.PrintErrf("Enabling %s on the cluster. This may take a few seconds, please wait.\n", strings.Join(args, ", ")) - if err := client.UpdateClusterConfig(cmd.Context(), request); err != nil { + ctx, cancel := context.WithTimeout(cmd.Context(), opts.timeout) + cobra.OnFinalize(cancel) + if err := client.UpdateClusterConfig(ctx, request); err != nil { cmd.PrintErrf("Error: Failed to enable %s on the cluster.\n\nThe error was: %v\n", strings.Join(args, ", "), err) env.Exit(1) return @@ -90,6 +101,7 @@ func newEnableCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { } cmd.Flags().StringVar(&opts.outputFormat, "output-format", "plain", "set the output format to one of plain, json or yaml") + cmd.Flags().DurationVar(&opts.timeout, "timeout", 90*time.Second, "the max time to wait for the command to execute") return cmd } diff --git a/src/k8s/cmd/k8s/k8s_generate_auth_token.go b/src/k8s/cmd/k8s/k8s_generate_auth_token.go index 1437cc823..c878f027d 100644 --- a/src/k8s/cmd/k8s/k8s_generate_auth_token.go +++ b/src/k8s/cmd/k8s/k8s_generate_auth_token.go @@ -1,6 +1,8 @@ package k8s import ( + "time" + apiv1 "github.com/canonical/k8s/api/v1" cmdutil "github.com/canonical/k8s/cmd/util" "github.com/spf13/cobra" @@ -10,6 +12,7 @@ func newGenerateAuthTokenCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { var opts struct { username string groups []string + timeout time.Duration } cmd := &cobra.Command{ @@ -17,6 +20,11 @@ func newGenerateAuthTokenCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { Hidden: true, PreRun: chainPreRunHooks(hookRequireRoot(env)), Run: func(cmd *cobra.Command, args []string) { + if opts.timeout < minTimeout { + cmd.PrintErrf("Timeout %v is less than minimum of %v. Using the minimum %v instead.\n", opts.timeout, minTimeout, minTimeout) + opts.timeout = minTimeout + } + client, err := env.Client(cmd.Context()) if err != nil { cmd.PrintErrf("Error: Failed to create a k8sd client. Make sure that the k8sd service is running.\n\nThe error was: %v\n", err) @@ -35,5 +43,6 @@ func newGenerateAuthTokenCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { } cmd.Flags().StringVar(&opts.username, "username", "", "Username") cmd.Flags().StringSliceVar(&opts.groups, "groups", nil, "Groups") + cmd.Flags().DurationVar(&opts.timeout, "timeout", 90*time.Second, "the max time to wait for the command to execute") return cmd } diff --git a/src/k8s/cmd/k8s/k8s_get.go b/src/k8s/cmd/k8s/k8s_get.go index 5b81616ae..374f4cc95 100644 --- a/src/k8s/cmd/k8s/k8s_get.go +++ b/src/k8s/cmd/k8s/k8s_get.go @@ -1,8 +1,10 @@ package k8s import ( + "context" "fmt" "strings" + "time" apiv1 "github.com/canonical/k8s/api/v1" cmdutil "github.com/canonical/k8s/cmd/util" @@ -12,6 +14,7 @@ import ( func newGetCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { var opts struct { outputFormat string + timeout time.Duration } cmd := &cobra.Command{ Use: "get ", @@ -20,6 +23,11 @@ func newGetCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { Args: cmdutil.MaximumNArgs(env, 1), PreRun: chainPreRunHooks(hookRequireRoot(env), hookInitializeFormatter(env, &opts.outputFormat)), Run: func(cmd *cobra.Command, args []string) { + if opts.timeout < minTimeout { + cmd.PrintErrf("Timeout %v is less than minimum of %v. Using the minimum %v instead.\n", opts.timeout, minTimeout, minTimeout) + opts.timeout = minTimeout + } + client, err := env.Client(cmd.Context()) if err != nil { cmd.PrintErrf("Error: Failed to create a k8sd client. Make sure that the k8sd service is running.\n\nThe error was: %v\n", err) @@ -27,7 +35,10 @@ func newGetCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { return } - config, err := client.GetClusterConfig(cmd.Context(), apiv1.GetClusterConfigRequest{}) + ctx, cancel := context.WithTimeout(cmd.Context(), opts.timeout) + cobra.OnFinalize(cancel) + + config, err := client.GetClusterConfig(ctx, apiv1.GetClusterConfigRequest{}) if err != nil { cmd.PrintErrf("Error: Failed to get the current cluster configuration.\n\nThe error was: %v\n", err) env.Exit(1) @@ -112,6 +123,7 @@ func newGetCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { }, } cmd.Flags().StringVar(&opts.outputFormat, "output-format", "plain", "set the output format to one of plain, json or yaml") + cmd.Flags().DurationVar(&opts.timeout, "timeout", 90*time.Second, "the max time to wait for the command to execute") return cmd } diff --git a/src/k8s/cmd/k8s/k8s_get_join_token.go b/src/k8s/cmd/k8s/k8s_get_join_token.go index f4c32655f..ab52ddba1 100644 --- a/src/k8s/cmd/k8s/k8s_get_join_token.go +++ b/src/k8s/cmd/k8s/k8s_get_join_token.go @@ -1,6 +1,9 @@ package k8s import ( + "context" + "time" + apiv1 "github.com/canonical/k8s/api/v1" cmdutil "github.com/canonical/k8s/cmd/util" "github.com/spf13/cobra" @@ -8,7 +11,8 @@ import ( func newGetJoinTokenCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { var opts struct { - worker bool + worker bool + timeout time.Duration } cmd := &cobra.Command{ Use: "get-join-token ", @@ -18,6 +22,11 @@ func newGetJoinTokenCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { Run: func(cmd *cobra.Command, args []string) { name := args[0] + if opts.timeout < minTimeout { + cmd.PrintErrf("Timeout %v is less than minimum of %v. Using the minimum %v instead.\n", opts.timeout, minTimeout, minTimeout) + opts.timeout = minTimeout + } + client, err := env.Client(cmd.Context()) if err != nil { cmd.PrintErrf("Error: Failed to create a k8sd client. Make sure that the k8sd service is running.\n\nThe error was: %v\n", err) @@ -25,7 +34,9 @@ func newGetJoinTokenCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { return } - token, err := client.GetJoinToken(cmd.Context(), apiv1.GetJoinTokenRequest{Name: name, Worker: opts.worker}) + ctx, cancel := context.WithTimeout(cmd.Context(), opts.timeout) + cobra.OnFinalize(cancel) + token, err := client.GetJoinToken(ctx, apiv1.GetJoinTokenRequest{Name: name, Worker: opts.worker}) if err != nil { cmd.PrintErrf("Error: Could not generate a join token for %q.\n\nThe error was: %v\n", name, err) env.Exit(1) @@ -37,5 +48,6 @@ func newGetJoinTokenCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { } cmd.Flags().BoolVar(&opts.worker, "worker", false, "generate a join token for a worker node") + cmd.Flags().DurationVar(&opts.timeout, "timeout", 90*time.Second, "the max time to wait for the command to execute") return cmd } diff --git a/src/k8s/cmd/k8s/k8s_join_cluster.go b/src/k8s/cmd/k8s/k8s_join_cluster.go index f64636621..148f32601 100644 --- a/src/k8s/cmd/k8s/k8s_join_cluster.go +++ b/src/k8s/cmd/k8s/k8s_join_cluster.go @@ -1,9 +1,11 @@ package k8s import ( + "context" "fmt" "io" "os" + "time" apiv1 "github.com/canonical/k8s/api/v1" cmdutil "github.com/canonical/k8s/cmd/util" @@ -26,6 +28,7 @@ func newJoinClusterCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { address string configFile string outputFormat string + timeout time.Duration } cmd := &cobra.Command{ Use: "join-cluster ", @@ -35,6 +38,11 @@ func newJoinClusterCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { Run: func(cmd *cobra.Command, args []string) { token := args[0] + if opts.timeout < minTimeout { + cmd.PrintErrf("Timeout %v is less than minimum of %v. Using the minimum %v instead.\n", opts.timeout, minTimeout, minTimeout) + opts.timeout = minTimeout + } + // Use hostname as default node name if opts.name == "" { // TODO(neoaggelos): use the encoded node name from the token, if available. @@ -88,8 +96,11 @@ func newJoinClusterCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { joinClusterConfig = string(b) } + ctx, cancel := context.WithTimeout(cmd.Context(), opts.timeout) + cobra.OnFinalize(cancel) + cmd.PrintErrln("Joining the cluster. This may take a few seconds, please wait.") - if err := client.JoinCluster(cmd.Context(), apiv1.JoinClusterRequest{Name: opts.name, Address: opts.address, Token: token, Config: joinClusterConfig}); err != nil { + if err := client.JoinCluster(ctx, apiv1.JoinClusterRequest{Name: opts.name, Address: opts.address, Token: token, Config: joinClusterConfig}); err != nil { cmd.PrintErrf("Error: Failed to join the cluster using the provided token.\n\nThe error was: %v\n", err) env.Exit(1) return @@ -102,5 +113,6 @@ func newJoinClusterCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { cmd.Flags().StringVar(&opts.address, "address", "", "microcluster address, defaults to the node IP address") cmd.Flags().StringVar(&opts.configFile, "file", "", "path to the YAML file containing your custom cluster join configuration. Use '-' to read from stdin.") cmd.Flags().StringVar(&opts.outputFormat, "output-format", "plain", "set the output format to one of plain, json or yaml") + cmd.Flags().DurationVar(&opts.timeout, "timeout", 90*time.Second, "the max time to wait for the command to execute") return cmd } diff --git a/src/k8s/cmd/k8s/k8s_remove_node.go b/src/k8s/cmd/k8s/k8s_remove_node.go index 70ff13074..9f6780361 100644 --- a/src/k8s/cmd/k8s/k8s_remove_node.go +++ b/src/k8s/cmd/k8s/k8s_remove_node.go @@ -1,7 +1,9 @@ package k8s import ( + "context" "fmt" + "time" apiv1 "github.com/canonical/k8s/api/v1" cmdutil "github.com/canonical/k8s/cmd/util" @@ -20,6 +22,7 @@ func newRemoveNodeCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { var opts struct { force bool outputFormat string + timeout time.Duration } cmd := &cobra.Command{ Use: "remove-node ", @@ -27,6 +30,11 @@ func newRemoveNodeCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { PreRun: chainPreRunHooks(hookRequireRoot(env), hookInitializeFormatter(env, &opts.outputFormat)), Args: cmdutil.ExactArgs(env, 1), Run: func(cmd *cobra.Command, args []string) { + if opts.timeout < minTimeout { + cmd.PrintErrf("Timeout %v is less than minimum of %v. Using the minimum %v instead.\n", opts.timeout, minTimeout, minTimeout) + opts.timeout = minTimeout + } + client, err := env.Client(cmd.Context()) if err != nil { cmd.PrintErrf("Error: Failed to create a k8sd client. Make sure that the k8sd service is running.\n\nThe error was: %v\n", err) @@ -36,8 +44,11 @@ func newRemoveNodeCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { name := args[0] + ctx, cancel := context.WithTimeout(cmd.Context(), opts.timeout) + cobra.OnFinalize(cancel) + cmd.PrintErrf("Removing %q from the Kubernetes cluster. This may take a few seconds, please wait.\n", name) - if err := client.RemoveNode(cmd.Context(), apiv1.RemoveNodeRequest{Name: name, Force: opts.force}); err != nil { + if err := client.RemoveNode(ctx, apiv1.RemoveNodeRequest{Name: name, Force: opts.force}); err != nil { cmd.PrintErrf("Error: Failed to remove node %q from the cluster.\n\nThe error was: %v\n", name, err) env.Exit(1) return @@ -49,5 +60,7 @@ func newRemoveNodeCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { cmd.Flags().BoolVar(&opts.force, "force", false, "forcibly remove the cluster member") cmd.Flags().StringVar(&opts.outputFormat, "output-format", "plain", "set the output format to one of plain, json or yaml") + cmd.Flags().DurationVar(&opts.timeout, "timeout", 90*time.Second, "the max time to wait for the command to execute") + return cmd } diff --git a/src/k8s/cmd/k8s/k8s_set.go b/src/k8s/cmd/k8s/k8s_set.go index 46750f2a6..5c2fc059b 100644 --- a/src/k8s/cmd/k8s/k8s_set.go +++ b/src/k8s/cmd/k8s/k8s_set.go @@ -1,8 +1,10 @@ package k8s import ( + "context" "fmt" "strings" + "time" apiv1 "github.com/canonical/k8s/api/v1" cmdutil "github.com/canonical/k8s/cmd/util" @@ -22,6 +24,7 @@ func (s SetResult) String() string { func newSetCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { var opts struct { outputFormat string + timeout time.Duration } cmd := &cobra.Command{ Use: "set ...", @@ -50,7 +53,10 @@ func newSetCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { Config: config, } - if err := client.UpdateClusterConfig(cmd.Context(), request); err != nil { + ctx, cancel := context.WithTimeout(cmd.Context(), opts.timeout) + cobra.OnFinalize(cancel) + + if err := client.UpdateClusterConfig(ctx, request); err != nil { cmd.PrintErrf("Error: Failed to apply requested cluster configuration changes.\n\nThe error was: %v\n", err) env.Exit(1) return @@ -61,35 +67,36 @@ func newSetCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { } cmd.Flags().StringVar(&opts.outputFormat, "output-format", "plain", "set the output format to one of plain, json or yaml") + cmd.Flags().DurationVar(&opts.timeout, "timeout", 90*time.Second, "the max time to wait for the command to execute") return cmd } var knownSetKeys = map[string]struct{}{ - "cloud-provider": struct{}{}, - "dns.cluster-domain": struct{}{}, - "dns.enabled": struct{}{}, - "dns.service-ip": struct{}{}, - "dns.upstream-nameservers": struct{}{}, - "gateway.enabled": struct{}{}, - "ingress.default-tls-secret": struct{}{}, - "ingress.enable-proxy-protocol": struct{}{}, - "ingress.enabled": struct{}{}, - "load-balancer.bgp-local-asn": struct{}{}, - "load-balancer.bgp-mode": struct{}{}, - "load-balancer.bgp-peer-address": struct{}{}, - "load-balancer.bgp-peer-asn": struct{}{}, - "load-balancer.bgp-peer-port": struct{}{}, - "load-balancer.cidrs": struct{}{}, - "load-balancer.enabled": struct{}{}, - "load-balancer.l2-interfaces": struct{}{}, - "load-balancer.l2-mode": struct{}{}, - "local-storage.default": struct{}{}, - "local-storage.enabled": struct{}{}, - "local-storage.local-path": struct{}{}, - "local-storage.reclaim-policy": struct{}{}, - "metrics-server.enabled": struct{}{}, - "network.enabled": struct{}{}, + "cloud-provider": {}, + "dns.cluster-domain": {}, + "dns.enabled": {}, + "dns.service-ip": {}, + "dns.upstream-nameservers": {}, + "gateway.enabled": {}, + "ingress.default-tls-secret": {}, + "ingress.enable-proxy-protocol": {}, + "ingress.enabled": {}, + "load-balancer.bgp-local-asn": {}, + "load-balancer.bgp-mode": {}, + "load-balancer.bgp-peer-address": {}, + "load-balancer.bgp-peer-asn": {}, + "load-balancer.bgp-peer-port": {}, + "load-balancer.cidrs": {}, + "load-balancer.enabled": {}, + "load-balancer.l2-interfaces": {}, + "load-balancer.l2-mode": {}, + "local-storage.default": {}, + "local-storage.enabled": {}, + "local-storage.local-path": {}, + "local-storage.reclaim-policy": {}, + "metrics-server.enabled": {}, + "network.enabled": {}, } func updateConfigMapstructure(config *apiv1.UserFacingClusterConfig, arg string) error { diff --git a/src/k8s/cmd/k8s/k8s_status.go b/src/k8s/cmd/k8s/k8s_status.go index cae5a9052..a52786d62 100644 --- a/src/k8s/cmd/k8s/k8s_status.go +++ b/src/k8s/cmd/k8s/k8s_status.go @@ -1,6 +1,9 @@ package k8s import ( + "context" + "time" + apiv1 "github.com/canonical/k8s/api/v1" cmdutil "github.com/canonical/k8s/cmd/util" "github.com/spf13/cobra" @@ -10,12 +13,18 @@ func newStatusCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { var opts struct { waitReady bool outputFormat string + timeout time.Duration } cmd := &cobra.Command{ Use: "status", Short: "Retrieve the current status of the cluster", PreRun: chainPreRunHooks(hookRequireRoot(env), hookInitializeFormatter(env, &opts.outputFormat)), Run: func(cmd *cobra.Command, args []string) { + if opts.timeout < minTimeout { + cmd.PrintErrf("Timeout %v is less than minimum of %v. Using the minimum %v instead.\n", opts.timeout, minTimeout, minTimeout) + opts.timeout = minTimeout + } + client, err := env.Client(cmd.Context()) if err != nil { cmd.PrintErrf("Error: Failed to create a k8sd client. Make sure that the k8sd service is running.\n\nThe error was: %v\n", err) @@ -23,13 +32,16 @@ func newStatusCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { return } - if !client.IsBootstrapped(cmd.Context()) { + ctx, cancel := context.WithTimeout(cmd.Context(), opts.timeout) + cobra.OnFinalize(cancel) + + if !client.IsBootstrapped(ctx) { cmd.PrintErrln("Error: The node is not part of a Kubernetes cluster. You can bootstrap a new cluster with:\n\n sudo k8s bootstrap") env.Exit(1) return } - status, err := client.ClusterStatus(cmd.Context(), opts.waitReady) + status, err := client.ClusterStatus(ctx, opts.waitReady) if err != nil { cmd.PrintErrf("Error: Failed to retrieve the cluster status.\n\nThe error was: %v\n", err) env.Exit(1) @@ -45,5 +57,6 @@ func newStatusCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { cmd.Flags().BoolVar(&opts.waitReady, "wait-ready", false, "wait until at least one cluster node is ready") cmd.Flags().StringVar(&opts.outputFormat, "output-format", "plain", "set the output format to one of plain, json or yaml") + cmd.Flags().DurationVar(&opts.timeout, "timeout", 90*time.Second, "the max time to wait for the command to execute") return cmd }