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