From 5b0aa816636c316818a128e81cdbaf5d3b7208c6 Mon Sep 17 00:00:00 2001 From: Lubron Date: Tue, 25 Aug 2026 12:23:55 -0700 Subject: [PATCH] feat(cli): add velero client config set namespace-mode=auto (#10127) * Issue #3194: Add velero client set-context-as-velero-namespace command Saves the namespace of the current (or a specified) kubeconfig context into the Velero client config file, so operational commands default to it without requiring --namespace on every invocation. Co-Authored-By: Claude Sonnet 5 Signed-off-by: lubronzhan * Fix CI: rename changelog to PR number, add unit tests for coverage - changelogs/unreleased must be named -; rename from the 0000 placeholder to 10127 to satisfy hack/changelog-check.sh. - Extract the command's logic into setContextAsVeleroNamespace so it's testable without triggering os.Exit via cmd.CheckError, and add unit tests covering: namespace read from context, context with no explicit namespace, overwriting an existing config value, and invalid kubeconfig path. Addresses 0% codecov patch coverage on the PR. Co-Authored-By: Claude Sonnet 5 Signed-off-by: lubronzhan * Move set-namespace-from-context under client config Shubham suggested nesting the new command under `config` for hierarchy consistency, and renaming it since the original set-context-as-velero-namespace name was long and ambiguous. Moves it to `velero client config set-namespace-from-context`, matching the existing config get/set subcommands and my follow-up naming suggestion on the review thread. AI-Tool-Used: Claude Code AI-Tool-Use-Level: Category 3 (Low) AI-Code-Category: Category 1 (Production) Signed-off-by: lubronzhan * Replace set-namespace-from-context with namespace-mode=auto kaovilai noted on #10127 that a one-shot command to snapshot the kubecontext namespace becomes redundant once a config toggle can resolve it dynamically, and isn't much simpler than the existing `config set namespace=...` alternative. Drop the dedicated set-namespace-from-context subcommand and instead teach the client Factory to resolve the operational namespace from the current kubeconfig context on every invocation when `namespace-mode=auto` is set via the existing generic `config set` command. Explicit --namespace flags and VELERO_NAMESPACE still take precedence, so the new mode only changes behavior when neither is set. AI-Tool-Used: Claude Code AI-Tool-Use-Level: Category 2 (Medium) AI-Code-Category: Category 1 (Production) Signed-off-by: lubronzhan * Address PR review: doc, fallback test, t.Setenv Resolve feedback from PR #10127 review 4966054980: - Document how to disable namespace-mode=auto (namespace-mode=) and note the fallback to the static namespace, in namespace.md. - Add a factory test covering the fallback to the stored/default namespace when kubeconfig namespace resolution fails. - Switch the VELERO_NAMESPACE override test to t.Setenv, wrapped in a subtest so its cleanup runs before later tests execute. AI-Tool-Used: Claude Code AI-Tool-Use-Level: Category 2 (Medium) AI-Code-Category: Category 2 (Non-Production) Signed-off-by: lubronzhan --------- Signed-off-by: lubronzhan Co-authored-by: Claude Sonnet 5 --- changelogs/unreleased/10127-lubronzhan | 1 + pkg/client/client.go | 17 +++++++++++ pkg/client/config.go | 28 +++++++++++++++--- pkg/client/factory.go | 30 ++++++++++++++------ pkg/client/factory_test.go | 39 ++++++++++++++++++++++++++ site/content/docs/main/namespace.md | 14 +++++++++ 6 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 changelogs/unreleased/10127-lubronzhan diff --git a/changelogs/unreleased/10127-lubronzhan b/changelogs/unreleased/10127-lubronzhan new file mode 100644 index 000000000..9ac64d9d3 --- /dev/null +++ b/changelogs/unreleased/10127-lubronzhan @@ -0,0 +1 @@ +Add `velero client config set namespace-mode=auto` to make operational commands resolve their default namespace from the current kubeconfig context on every invocation diff --git a/pkg/client/client.go b/pkg/client/client.go index 39cdc9141..e49fbd0cc 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -58,6 +58,23 @@ func Config(kubeconfig, kubecontext, baseName string, qps float32, burst int) (* return clientConfig, nil } +// NamespaceFromKubeContext returns the namespace associated with the given kubeconfig context +// (or the current context if kubecontext is empty), using the given kubeconfig file (or the +// default loading rules if kubeconfig is empty). +func NamespaceFromKubeContext(kubeconfig, kubecontext string) (string, error) { + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + loadingRules.ExplicitPath = kubeconfig + configOverrides := &clientcmd.ConfigOverrides{CurrentContext: kubecontext} + kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) + + namespace, _, err := kubeConfig.Namespace() + if err != nil { + return "", errors.Wrap(err, "error finding namespace in --kubeconfig, $KUBECONFIG, or in-cluster configuration") + } + + return namespace, nil +} + // buildUserAgent builds a User-Agent string from given args. func buildUserAgent(command, version, formattedSha, os, arch string) string { return fmt.Sprintf( diff --git a/pkg/client/config.go b/pkg/client/config.go index 2a96e3467..793c4b9a0 100644 --- a/pkg/client/config.go +++ b/pkg/client/config.go @@ -27,10 +27,16 @@ import ( ) const ( - ConfigKeyNamespace = "namespace" - ConfigKeyFeatures = "features" - ConfigKeyCACert = "cacert" - ConfigKeyColorized = "colorized" + ConfigKeyNamespace = "namespace" + ConfigKeyNamespaceMode = "namespace-mode" + ConfigKeyFeatures = "features" + ConfigKeyCACert = "cacert" + ConfigKeyColorized = "colorized" + + // NamespaceModeAuto is the ConfigKeyNamespaceMode value that makes Velero resolve the + // namespace for operational commands from the current kubeconfig context on every + // invocation, instead of the static ConfigKeyNamespace value. + NamespaceModeAuto = "auto" ) // VeleroConfig is a map of strings to any for deserializing Velero client config options. @@ -99,6 +105,20 @@ func (c VeleroConfig) Namespace() string { return ns } +func (c VeleroConfig) NamespaceMode() string { + val, ok := c[ConfigKeyNamespaceMode] + if !ok { + return "" + } + + mode, ok := val.(string) + if !ok { + return "" + } + + return mode +} + func (c VeleroConfig) Features() []string { val, ok := c[ConfigKeyFeatures] if !ok { diff --git a/pkg/client/factory.go b/pkg/client/factory.go index 17e2a243a..01df4ed7b 100644 --- a/pkg/client/factory.go +++ b/pkg/client/factory.go @@ -77,20 +77,22 @@ type Factory interface { } type factory struct { - flags *pflag.FlagSet - kubeconfig string - kubecontext string - baseName string - namespace string - clientQPS float32 - clientBurst int + flags *pflag.FlagSet + kubeconfig string + kubecontext string + baseName string + namespace string + namespaceMode string + clientQPS float32 + clientBurst int } // NewFactory returns a Factory. func NewFactory(baseName string, config VeleroConfig) Factory { f := &factory{ - flags: pflag.NewFlagSet("", pflag.ContinueOnError), - baseName: baseName, + flags: pflag.NewFlagSet("", pflag.ContinueOnError), + baseName: baseName, + namespaceMode: config.NamespaceMode(), } f.namespace = os.Getenv("VELERO_NAMESPACE") @@ -242,5 +244,15 @@ func (f *factory) SetClientBurst(burst int) { } func (f *factory) Namespace() string { + // In auto mode, the namespace is resolved from the current kubeconfig context on every + // call, unless the caller explicitly overrode it with --namespace or VELERO_NAMESPACE. + if f.namespaceMode == NamespaceModeAuto && + !f.flags.Changed("namespace") && + os.Getenv("VELERO_NAMESPACE") == "" { + if namespace, err := NamespaceFromKubeContext(f.kubeconfig, f.kubecontext); err == nil && namespace != "" { + return namespace + } + } + return f.namespace } diff --git a/pkg/client/factory_test.go b/pkg/client/factory_test.go index 5b9db37f1..2f63d3a3c 100644 --- a/pkg/client/factory_test.go +++ b/pkg/client/factory_test.go @@ -64,6 +64,45 @@ func TestFactory(t *testing.T) { os.Unsetenv("VELERO_NAMESPACE") + // namespace-mode=auto should resolve the namespace from the current kubeconfig context. + f = NewFactory("velero", VeleroConfig{ConfigKeyNamespaceMode: NamespaceModeAuto}) + flags = new(flag.FlagSet) + f.BindFlags(flags) + require.NoError(t, flags.Parse([]string{"--kubeconfig", "kubeconfig", "--kubecontext", "federal-context"})) + assert.Equal(t, "chisel-ns", f.Namespace()) + + // namespace-mode=auto should track kubecontext changes dynamically. + f = NewFactory("velero", VeleroConfig{ConfigKeyNamespaceMode: NamespaceModeAuto}) + flags = new(flag.FlagSet) + f.BindFlags(flags) + require.NoError(t, flags.Parse([]string{"--kubeconfig", "kubeconfig", "--kubecontext", "queen-anne-context"})) + assert.Equal(t, "saw-ns", f.Namespace()) + + // An explicit --namespace flag overrides namespace-mode=auto. + f = NewFactory("velero", VeleroConfig{ConfigKeyNamespaceMode: NamespaceModeAuto}) + flags = new(flag.FlagSet) + f.BindFlags(flags) + require.NoError(t, flags.Parse([]string{"--kubeconfig", "kubeconfig", "--kubecontext", "federal-context", "--namespace", s})) + assert.Equal(t, s, f.Namespace()) + + // VELERO_NAMESPACE overrides namespace-mode=auto. + t.Run("VELERO_NAMESPACE overrides namespace-mode=auto", func(t *testing.T) { + t.Setenv("VELERO_NAMESPACE", "env-velero") + f := NewFactory("velero", VeleroConfig{ConfigKeyNamespaceMode: NamespaceModeAuto}) + flags := new(flag.FlagSet) + f.BindFlags(flags) + require.NoError(t, flags.Parse([]string{"--kubeconfig", "kubeconfig", "--kubecontext", "federal-context"})) + assert.Equal(t, "env-velero", f.Namespace()) + }) + + // namespace-mode=auto falls back to the stored/default namespace when the kubeconfig + // namespace can't be resolved (e.g. the kubeconfig file doesn't exist). + f = NewFactory("velero", VeleroConfig{ConfigKeyNamespace: "stored-ns", ConfigKeyNamespaceMode: NamespaceModeAuto}) + flags = new(flag.FlagSet) + f.BindFlags(flags) + require.NoError(t, flags.Parse([]string{"--kubeconfig", "nonexistent-kubeconfig"})) + assert.Equal(t, "stored-ns", f.Namespace()) + tests := []struct { name string kubeconfig string diff --git a/site/content/docs/main/namespace.md b/site/content/docs/main/namespace.md index 68561e720..70d84b09a 100644 --- a/site/content/docs/main/namespace.md +++ b/site/content/docs/main/namespace.md @@ -17,6 +17,20 @@ To have namespace consistency, specify the namespace for all Velero operational velero client config set namespace= ``` +If Velero was installed in the namespace of your current kubeconfig context, you can have operational commands automatically use that namespace, without having to type it out or update it every time you switch contexts: + +```bash +velero client config set namespace-mode=auto +``` + +With `namespace-mode=auto` set, Velero resolves the namespace from the current kubeconfig context (or the context specified with `--kubecontext`) on every command invocation, instead of using the static `namespace` value. If the namespace can't be resolved from the kubeconfig context (for example, the context has no namespace set, or the kubeconfig can't be loaded), Velero falls back to the static `namespace` value, or the `velero` default if that isn't set either. + +To disable `namespace-mode=auto` and go back to using the static `namespace` value, clear it by setting it to an empty value: + +```bash +velero client config set namespace-mode= +``` + Alternatively, you may use the global `--namespace` flag with any operational command to tell Velero where to run. [0]: basic-install.md#install-the-cli