diff --git a/cmd/non-admin/namespace.go b/cmd/non-admin/namespace.go new file mode 100644 index 00000000..0c7a2efc --- /dev/null +++ b/cmd/non-admin/namespace.go @@ -0,0 +1,83 @@ +/* +Copyright 2025 The OADP CLI Contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package nonadmin + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// ConfigureNamespaceBehavior hides the inherited --namespace flag from nonadmin +// help output and rejects runtime use of -n/--namespace. Nonadmin operations are +// scoped to the user's current context namespace for security. +func ConfigureNamespaceBehavior( + cmd *cobra.Command, + runGlobalSetup func(cmd *cobra.Command, args []string), +) { + hideNamespaceFlagFromCommand(cmd) + + existingPersistentPreRunE := cmd.PersistentPreRunE + existingPersistentPreRun := cmd.PersistentPreRun + + cmd.PersistentPreRunE = func(c *cobra.Command, args []string) error { + if c.Flags().Changed("namespace") { + return fmt.Errorf("-n/--namespace is not supported for nonadmin commands; namespace is determined by your current context") + } + if runGlobalSetup != nil { + runGlobalSetup(c, args) + } + if existingPersistentPreRunE != nil { + return existingPersistentPreRunE(c, args) + } + if existingPersistentPreRun != nil { + existingPersistentPreRun(c, args) + } + return nil + } +} + +func hideNamespaceFlagFromCommand(cmd *cobra.Command) { + originalHelpFunc := cmd.HelpFunc() + cmd.SetHelpFunc(func(c *cobra.Command, args []string) { + withHiddenNamespaceFlag(c, func() { + originalHelpFunc(c, args) + }) + }) + + originalUsageFunc := cmd.UsageFunc() + cmd.SetUsageFunc(func(c *cobra.Command) error { + var err error + withHiddenNamespaceFlag(c, func() { + err = originalUsageFunc(c) + }) + return err + }) + + for _, subCmd := range cmd.Commands() { + hideNamespaceFlagFromCommand(subCmd) + } +} + +func withHiddenNamespaceFlag(cmd *cobra.Command, fn func()) { + if flag := cmd.InheritedFlags().Lookup("namespace"); flag != nil { + originalHidden := flag.Hidden + flag.Hidden = true + defer func() { flag.Hidden = originalHidden }() + } + fn() +} diff --git a/cmd/non-admin/namespace_test.go b/cmd/non-admin/namespace_test.go new file mode 100644 index 00000000..1e2dcff5 --- /dev/null +++ b/cmd/non-admin/namespace_test.go @@ -0,0 +1,69 @@ +/* +Copyright 2025 The OADP CLI Contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package nonadmin + +import ( + "context" + "os/exec" + "strings" + "testing" + + "github.com/migtools/oadp-cli/internal/testutil" + "github.com/spf13/cobra" +) + +func TestNonAdminNamespaceFlagBehavior(t *testing.T) { + binaryPath := testutil.BuildCLIBinary(t) + + t.Run("help hides namespace flag", func(t *testing.T) { + output, _ := testutil.RunCommand(t, binaryPath, "nonadmin", "backup", "get", "--help") + if strings.Contains(output, "-n, --namespace") || strings.Contains(output, "--namespace string") { + t.Errorf("Expected help output not to contain the namespace flag\nFull output:\n%s", output) + } + }) + + t.Run("calls global persistent setup", func(t *testing.T) { + var globalSetupCalled bool + globalSetup := func(cmd *cobra.Command, args []string) { + globalSetupCalled = true + } + + cmd := &cobra.Command{Use: "nonadmin"} + ConfigureNamespaceBehavior(cmd, globalSetup) + + if err := cmd.PersistentPreRunE(cmd, []string{}); err != nil { + t.Fatalf("PersistentPreRunE returned error: %v", err) + } + if !globalSetupCalled { + t.Error("Expected global persistent setup to run for nonadmin command") + } + }) + + t.Run("rejects namespace flag at runtime", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), testutil.TestTimeout) + defer cancel() + + output, err := exec.CommandContext(ctx, binaryPath, "nonadmin", "backup", "get", "-n", "other-namespace").CombinedOutput() + if err == nil { + t.Fatalf("Expected error when -n/--namespace is provided, got output:\n%s", string(output)) + } + expected := "-n/--namespace is not supported for nonadmin commands; namespace is determined by your current context" + if !strings.Contains(string(output), expected) { + t.Errorf("Expected output to contain %q, got:\n%s", expected, string(output)) + } + }) +} diff --git a/cmd/root.go b/cmd/root.go index 429827a4..74e43f18 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -375,6 +375,24 @@ func isNonadminEnabled(config clientcmd.VeleroConfig) bool { } } +func configureGlobalCommandBehavior( + config clientcmd.VeleroConfig, + cmdFeatures veleroflag.StringArray, + cmdColorized veleroflag.OptionalBool, +) func(cmd *cobra.Command, args []string) { + return func(cmd *cobra.Command, args []string) { + features.Enable(config.Features()...) + features.Enable(cmdFeatures...) + + switch { + case cmdColorized.Value != nil: + color.NoColor = !*cmdColorized.Value + default: + color.NoColor = !config.Colorized() + } + } +} + // NewVeleroRootCommand returns a root command with all Velero CLI subcommands attached. func NewVeleroRootCommand(baseName string) *cobra.Command { @@ -400,6 +418,8 @@ func NewVeleroRootCommand(baseName string) *cobra.Command { var cmdFeatures veleroflag.StringArray var cmdColorzied veleroflag.OptionalBool + globalPreRun := configureGlobalCommandBehavior(config, cmdFeatures, cmdColorzied) + c := &cobra.Command{ Use: baseName, Short: "OADP CLI commands", @@ -407,17 +427,7 @@ func NewVeleroRootCommand(baseName string) *cobra.Command { // Default action when no subcommand is provided fmt.Println("Welcome to the OADP CLI! Use --help to see available commands.") }, - PersistentPreRun: func(cmd *cobra.Command, args []string) { - features.Enable(config.Features()...) - features.Enable(cmdFeatures...) - - switch { - case cmdColorzied.Value != nil: - color.NoColor = !*cmdColorzied.Value - default: - color.NoColor = !config.Colorized() - } - }, + PersistentPreRun: globalPreRun, } // Create Velero client factory for regular Velero commands @@ -451,7 +461,9 @@ func NewVeleroRootCommand(baseName string) *cobra.Command { c.AddCommand(nabsl.NewNABSLRequestCommand(f)) // Custom subcommands - use NonAdmin factory - c.AddCommand(nonadmin.NewNonAdminCommand(f)) + nonadminCmd := nonadmin.NewNonAdminCommand(f) + nonadmin.ConfigureNamespaceBehavior(nonadminCmd, globalPreRun) + c.AddCommand(nonadminCmd) // Must-gather command - diagnostic tool c.AddCommand(mustgather.NewMustGatherCommand(f))