From 4a9350ba0000ac489c58afacf25371efe8072806 Mon Sep 17 00:00:00 2001 From: Benjamin Schimke Date: Mon, 27 May 2024 17:32:10 +0200 Subject: [PATCH] Explicitly set timeout in each CLI command (#443) * Explicitly set timeout in each CLI command Right now, the context deadline is initially set at root level when the command is created. This causes problems for interactive commands as the deadline may be exceeded while the user inputs the data. To prevent this, we remove the global timeout and specifically set it in each command right before the RPC calls. * update docs --- docs/src/_parts/commands/k8s_bootstrap.md | 1 + docs/src/_parts/commands/k8s_disable.md | 1 + docs/src/_parts/commands/k8s_enable.md | 1 + .../src/_parts/commands/k8s_get-join-token.md | 5 +- docs/src/_parts/commands/k8s_get.md | 1 + docs/src/_parts/commands/k8s_join-cluster.md | 1 + docs/src/_parts/commands/k8s_remove-node.md | 1 + docs/src/_parts/commands/k8s_set.md | 1 + docs/src/_parts/commands/k8s_status.md | 1 + src/k8s/cmd/k8s/k8s.go | 22 +------ src/k8s/cmd/k8s/k8s_bootstrap.go | 14 ++++- src/k8s/cmd/k8s/k8s_config.go | 17 +++++- src/k8s/cmd/k8s/k8s_disable.go | 16 +++++- src/k8s/cmd/k8s/k8s_enable.go | 16 +++++- src/k8s/cmd/k8s/k8s_generate_auth_token.go | 9 +++ src/k8s/cmd/k8s/k8s_get.go | 14 ++++- src/k8s/cmd/k8s/k8s_get_join_token.go | 16 +++++- src/k8s/cmd/k8s/k8s_join_cluster.go | 14 ++++- src/k8s/cmd/k8s/k8s_remove_node.go | 15 ++++- src/k8s/cmd/k8s/k8s_set.go | 57 +++++++++++-------- src/k8s/cmd/k8s/k8s_status.go | 17 +++++- 21 files changed, 179 insertions(+), 61 deletions(-) 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 }