diff --git a/.gitignore b/.gitignore index 5d5cd8d..faf3a68 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ oadp-v*.yaml go.work # IDE files +.claude/ .vscode/ .idea/ *.swp diff --git a/Containerfile.download b/Containerfile.download index d277718..2e19c96 100644 --- a/Containerfile.download +++ b/Containerfile.download @@ -1,5 +1,5 @@ # This Dockerfile is used to cross-build the kubectl-oadp binaries for all platforms -# It also builds a Go server that serves the binaries for download +# It also builds a Go server that serves the built binaries FROM --platform=$BUILDPLATFORM golang:1.24 AS builder diff --git a/cmd/non-admin/bsl/bsl.go b/cmd/non-admin/bsl/bsl.go index 57011e2..b8eed27 100644 --- a/cmd/non-admin/bsl/bsl.go +++ b/cmd/non-admin/bsl/bsl.go @@ -31,6 +31,7 @@ func NewBSLCommand(f client.Factory) *cobra.Command { c.AddCommand( NewCreateCommand(f), + NewGetCommand(f, "get"), ) return c diff --git a/cmd/non-admin/bsl/bsl_test.go b/cmd/non-admin/bsl/bsl_test.go new file mode 100644 index 0000000..777a89a --- /dev/null +++ b/cmd/non-admin/bsl/bsl_test.go @@ -0,0 +1,352 @@ +/* +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 bsl + +import ( + "testing" + + "github.com/migtools/oadp-cli/internal/testutil" +) + +// TestNonAdminBSLCommands tests the non-admin BSL command functionality +func TestNonAdminBSLCommands(t *testing.T) { + binaryPath := testutil.BuildCLIBinary(t) + + tests := []struct { + name string + args []string + expectContains []string + }{ + { + name: "nonadmin bsl help", + args: []string{"nonadmin", "bsl", "--help"}, + expectContains: []string{ + "Create and manage non-admin backup storage locations", + "create", + "get", + }, + }, + { + name: "nonadmin bsl create help", + args: []string{"nonadmin", "bsl", "create", "--help"}, + expectContains: []string{ + "Create a non-admin backup storage location", + "--provider", + "--bucket", + "--credential", + "--region", + "--prefix", + }, + }, + { + name: "nonadmin bsl get help", + args: []string{"nonadmin", "bsl", "get", "--help"}, + expectContains: []string{ + "Get one or more non-admin backup storage locations", + }, + }, + { + name: "na bsl shorthand help", + args: []string{"na", "bsl", "--help"}, + expectContains: []string{ + "Create and manage non-admin backup storage locations", + "create", + "get", + }, + }, + // Verb-noun order help command tests + { + name: "nonadmin get bsl help", + args: []string{"nonadmin", "get", "bsl", "--help"}, + expectContains: []string{ + "Get one or more non-admin resources", + "bsl", + }, + }, + { + name: "nonadmin create bsl help", + args: []string{"nonadmin", "create", "bsl", "--help"}, + expectContains: []string{ + "Create non-admin resources", + "bsl", + }, + }, + // Shorthand verb-noun order tests + { + name: "na get bsl help", + args: []string{"na", "get", "bsl", "--help"}, + expectContains: []string{ + "Get one or more non-admin resources", + "bsl", + }, + }, + { + name: "na create bsl help", + args: []string{"na", "create", "bsl", "--help"}, + expectContains: []string{ + "Create non-admin resources", + "bsl", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.TestHelpCommand(t, binaryPath, tt.args, tt.expectContains) + }) + } +} + +// TestNonAdminBSLHelpFlags tests that both --help and -h work for BSL commands +func TestNonAdminBSLHelpFlags(t *testing.T) { + binaryPath := testutil.BuildCLIBinary(t) + + commands := [][]string{ + {"nonadmin", "bsl", "--help"}, + {"nonadmin", "bsl", "-h"}, + {"nonadmin", "bsl", "create", "--help"}, + {"nonadmin", "bsl", "create", "-h"}, + {"nonadmin", "bsl", "get", "--help"}, + {"nonadmin", "bsl", "get", "-h"}, + {"na", "bsl", "--help"}, + {"na", "bsl", "-h"}, + // Verb-noun order help flags + {"nonadmin", "get", "bsl", "--help"}, + {"nonadmin", "get", "bsl", "-h"}, + {"nonadmin", "create", "bsl", "--help"}, + {"nonadmin", "create", "bsl", "-h"}, + // Shorthand verb-noun order help flags + {"na", "get", "bsl", "--help"}, + {"na", "get", "bsl", "-h"}, + {"na", "create", "bsl", "--help"}, + {"na", "create", "bsl", "-h"}, + } + + for _, cmd := range commands { + t.Run("help_flags_"+cmd[len(cmd)-1], func(t *testing.T) { + testutil.TestHelpCommand(t, binaryPath, cmd, []string{"Usage:"}) + }) + } +} + +// TestNonAdminBSLCreateFlags tests create command specific flags +func TestNonAdminBSLCreateFlags(t *testing.T) { + binaryPath := testutil.BuildCLIBinary(t) + + t.Run("create command has all expected flags", func(t *testing.T) { + expectedFlags := []string{ + "--provider", + "--bucket", + "--credential", + "--region", + "--prefix", + "--config", + } + + testutil.TestHelpCommand(t, binaryPath, + []string{"nonadmin", "bsl", "create", "--help"}, + expectedFlags) + }) +} + +// TestNonAdminBSLExamples tests that help text contains proper examples +func TestNonAdminBSLExamples(t *testing.T) { + binaryPath := testutil.BuildCLIBinary(t) + + t.Run("create examples use correct command format", func(t *testing.T) { + expectedExamples := []string{ + "kubectl oadp nonadmin bsl create", + "--provider", + "--bucket", + "--credential", + } + + testutil.TestHelpCommand(t, binaryPath, + []string{"nonadmin", "bsl", "create", "--help"}, + expectedExamples) + }) + + t.Run("get examples use correct command format", func(t *testing.T) { + expectedExamples := []string{ + "kubectl oadp nonadmin bsl get", + } + + testutil.TestHelpCommand(t, binaryPath, + []string{"nonadmin", "bsl", "get", "--help"}, + expectedExamples) + }) + + t.Run("main bsl help shows subcommands", func(t *testing.T) { + expectedSubcommands := []string{ + "create", + "get", + } + + testutil.TestHelpCommand(t, binaryPath, + []string{"nonadmin", "bsl", "--help"}, + expectedSubcommands) + }) +} + +// TestNonAdminBSLClientConfigIntegration tests that BSL commands respect client config +func TestNonAdminBSLClientConfigIntegration(t *testing.T) { + binaryPath := testutil.BuildCLIBinary(t) + _, cleanup := testutil.SetupTempHome(t) + defer cleanup() + + t.Run("bsl commands work with client config", func(t *testing.T) { + // Set a known namespace + _, err := testutil.RunCommand(t, binaryPath, "client", "config", "set", "namespace=user-namespace") + if err != nil { + t.Fatalf("Failed to set client config: %v", err) + } + + // Test that BSL commands can be invoked (they should respect the namespace) + // We test help commands since they don't require actual K8s resources + commands := [][]string{ + {"nonadmin", "bsl", "get", "--help"}, + {"nonadmin", "bsl", "create", "--help"}, + {"na", "bsl", "get", "--help"}, + // Verb-noun order commands + {"nonadmin", "get", "bsl", "--help"}, + {"nonadmin", "create", "bsl", "--help"}, + {"na", "get", "bsl", "--help"}, + {"na", "create", "bsl", "--help"}, + } + + for _, cmd := range commands { + t.Run("config_test_"+cmd[len(cmd)-2], func(t *testing.T) { + output, err := testutil.RunCommand(t, binaryPath, cmd...) + if err != nil { + t.Fatalf("Non-admin BSL command should work with client config: %v", err) + } + if output == "" { + t.Errorf("Expected help output for %v", cmd) + } + }) + } + }) +} + +// TestNonAdminBSLCommandStructure tests the overall command structure +func TestNonAdminBSLCommandStructure(t *testing.T) { + binaryPath := testutil.BuildCLIBinary(t) + + t.Run("bsl commands available under nonadmin", func(t *testing.T) { + _, err := testutil.RunCommand(t, binaryPath, "nonadmin", "--help") + if err != nil { + t.Fatalf("nonadmin command should exist: %v", err) + } + + expectedCommands := []string{"bsl"} + for _, cmd := range expectedCommands { + testutil.TestHelpCommand(t, binaryPath, []string{"nonadmin", "--help"}, []string{cmd}) + } + }) + + t.Run("bsl commands available under na shorthand", func(t *testing.T) { + _, err := testutil.RunCommand(t, binaryPath, "na", "--help") + if err != nil { + t.Fatalf("na command should exist: %v", err) + } + + expectedCommands := []string{"bsl"} + for _, cmd := range expectedCommands { + testutil.TestHelpCommand(t, binaryPath, []string{"na", "--help"}, []string{cmd}) + } + }) +} + +// TestVerbNounOrderBSLExamples tests that verb-noun order commands show proper BSL examples +func TestVerbNounOrderBSLExamples(t *testing.T) { + binaryPath := testutil.BuildCLIBinary(t) + + t.Run("get verb command shows bsl examples", func(t *testing.T) { + expectedExamples := []string{ + "kubectl oadp nonadmin get bsl", + } + + testutil.TestHelpCommand(t, binaryPath, + []string{"nonadmin", "get", "--help"}, + expectedExamples) + }) + + t.Run("create verb command shows bsl examples", func(t *testing.T) { + expectedExamples := []string{ + "kubectl oadp nonadmin create bsl", + } + + testutil.TestHelpCommand(t, binaryPath, + []string{"nonadmin", "create", "--help"}, + expectedExamples) + }) + + t.Run("get bsl with specific resource shows proper examples", func(t *testing.T) { + expectedExamples := []string{ + "kubectl oadp nonadmin get bsl", + } + + testutil.TestHelpCommand(t, binaryPath, + []string{"nonadmin", "get", "bsl", "--help"}, + expectedExamples) + }) + + t.Run("create bsl with specific resource shows proper examples", func(t *testing.T) { + expectedExamples := []string{ + "kubectl oadp nonadmin create bsl", + } + + testutil.TestHelpCommand(t, binaryPath, + []string{"nonadmin", "create", "bsl", "--help"}, + expectedExamples) + }) +} + +// TestNonAdminBSLOutputFormat tests that help text uses correct command format +func TestNonAdminBSLOutputFormat(t *testing.T) { + binaryPath := testutil.BuildCLIBinary(t) + + t.Run("usage shows oc oadp prefix", func(t *testing.T) { + expectedStrings := []string{ + "oc oadp nonadmin bsl", + } + + testutil.TestHelpCommand(t, binaryPath, + []string{"nonadmin", "bsl", "--help"}, + expectedStrings) + }) + + t.Run("create usage shows oc oadp prefix", func(t *testing.T) { + expectedStrings := []string{ + "oc oadp nonadmin bsl create", + } + + testutil.TestHelpCommand(t, binaryPath, + []string{"nonadmin", "bsl", "create", "--help"}, + expectedStrings) + }) + + t.Run("get usage shows oc oadp prefix", func(t *testing.T) { + expectedStrings := []string{ + "oc oadp nonadmin bsl get", + } + + testutil.TestHelpCommand(t, binaryPath, + []string{"nonadmin", "bsl", "get", "--help"}, + expectedStrings) + }) +} diff --git a/cmd/non-admin/bsl/create.go b/cmd/non-admin/bsl/create.go index 083ceef..0fc0c5a 100644 --- a/cmd/non-admin/bsl/create.go +++ b/cmd/non-admin/bsl/create.go @@ -200,6 +200,6 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error { fmt.Printf("NonAdminBackupStorageLocation %q created successfully.\n", nabsl.Name) fmt.Printf("The controller will create a request for admin approval.\n") - fmt.Printf("Use 'kubectl oadp nonadmin bsl request get' to view auto-created requests.\n") + fmt.Printf("Use 'oc oadp nonadmin bsl request get' to view auto-created requests.\n") return nil } diff --git a/cmd/non-admin/bsl/get.go b/cmd/non-admin/bsl/get.go new file mode 100644 index 0000000..8db5f29 --- /dev/null +++ b/cmd/non-admin/bsl/get.go @@ -0,0 +1,173 @@ +/* +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 bsl + +import ( + "context" + "fmt" + "time" + + "github.com/migtools/oadp-cli/cmd/shared" + nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" + "github.com/spf13/cobra" + "github.com/vmware-tanzu/velero/pkg/client" + "github.com/vmware-tanzu/velero/pkg/cmd/util/output" + kbclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +func NewGetCommand(f client.Factory, use string) *cobra.Command { + c := &cobra.Command{ + Use: use + " [NAME]", + Short: "Get non-admin backup storage location(s)", + Long: "Get one or more non-admin backup storage locations", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + // Get the current namespace from kubectl context + userNamespace, err := shared.GetCurrentNamespace() + if err != nil { + return fmt.Errorf("failed to determine current namespace: %w", err) + } + + // Create client with full scheme + kbClient, err := shared.NewClientWithFullScheme(f) + if err != nil { + return err + } + + if len(args) == 1 { + // Get specific BSL + bslName := args[0] + var nabsl nacv1alpha1.NonAdminBackupStorageLocation + err := kbClient.Get(context.Background(), kbclient.ObjectKey{ + Namespace: userNamespace, + Name: bslName, + }, &nabsl) + if err != nil { + return fmt.Errorf("failed to get NonAdminBackupStorageLocation %q: %w", bslName, err) + } + + if printed, err := output.PrintWithFormat(cmd, &nabsl); printed || err != nil { + return err + } + + // If no output format specified, print table format for single item + list := &nacv1alpha1.NonAdminBackupStorageLocationList{ + Items: []nacv1alpha1.NonAdminBackupStorageLocation{nabsl}, + } + return printNonAdminBSLTable(list) + } else { + // List all BSLs in namespace + var nabslList nacv1alpha1.NonAdminBackupStorageLocationList + err := kbClient.List(context.Background(), &nabslList, &kbclient.ListOptions{ + Namespace: userNamespace, + }) + if err != nil { + return fmt.Errorf("failed to list NonAdminBackupStorageLocations: %w", err) + } + + if printed, err := output.PrintWithFormat(cmd, &nabslList); printed || err != nil { + return err + } + + // Print table format + return printNonAdminBSLTable(&nabslList) + } + }, + Example: ` # Get all non-admin backup storage locations in the current namespace + kubectl oadp nonadmin bsl get + + # Get a specific non-admin backup storage location + kubectl oadp nonadmin bsl get my-storage + + # Get backup storage locations in YAML format + kubectl oadp nonadmin bsl get -o yaml + + # Get a specific backup storage location in JSON format + kubectl oadp nonadmin bsl get my-storage -o json`, + } + + output.BindFlags(c.Flags()) + output.ClearOutputFlagDefault(c) + + return c +} + +func printNonAdminBSLTable(nabslList *nacv1alpha1.NonAdminBackupStorageLocationList) error { + if len(nabslList.Items) == 0 { + fmt.Println("No non-admin backup storage locations found.") + return nil + } + + // Print header + fmt.Printf("%-30s %-15s %-15s %-20s %-10s\n", "NAME", "REQUEST PHASE", "PROVIDER", "BUCKET/PREFIX", "AGE") + + // Print each BSL + for _, nabsl := range nabslList.Items { + status := getBSLStatus(&nabsl) + provider := getProvider(&nabsl) + bucketPrefix := getBucketPrefix(&nabsl) + age := formatAge(nabsl.CreationTimestamp.Time) + + fmt.Printf("%-30s %-15s %-15s %-20s %-10s\n", nabsl.Name, status, provider, bucketPrefix, age) + } + + return nil +} + +func getBSLStatus(nabsl *nacv1alpha1.NonAdminBackupStorageLocation) string { + if nabsl.Status.Phase != "" { + return string(nabsl.Status.Phase) + } + return "Unknown" +} + +func getProvider(nabsl *nacv1alpha1.NonAdminBackupStorageLocation) string { + if nabsl.Spec.BackupStorageLocationSpec != nil && nabsl.Spec.BackupStorageLocationSpec.Provider != "" { + return nabsl.Spec.BackupStorageLocationSpec.Provider + } + return "N/A" +} + +func getBucketPrefix(nabsl *nacv1alpha1.NonAdminBackupStorageLocation) string { + if nabsl.Spec.BackupStorageLocationSpec != nil && nabsl.Spec.BackupStorageLocationSpec.ObjectStorage != nil { + bucket := nabsl.Spec.BackupStorageLocationSpec.ObjectStorage.Bucket + prefix := nabsl.Spec.BackupStorageLocationSpec.ObjectStorage.Prefix + if prefix != "" { + return fmt.Sprintf("%s/%s", bucket, prefix) + } + return bucket + } + return "N/A" +} + +func formatAge(t time.Time) string { + duration := time.Since(t) + + days := int(duration.Hours() / 24) + hours := int(duration.Hours()) % 24 + minutes := int(duration.Minutes()) % 60 + + if days > 0 { + return fmt.Sprintf("%dd", days) + } else if hours > 0 { + return fmt.Sprintf("%dh", hours) + } else if minutes > 0 { + return fmt.Sprintf("%dm", minutes) + } else { + return "1m" + } +} diff --git a/cmd/non-admin/verbs/registry.go b/cmd/non-admin/verbs/registry.go index 4156a1a..694f389 100644 --- a/cmd/non-admin/verbs/registry.go +++ b/cmd/non-admin/verbs/registry.go @@ -36,10 +36,9 @@ func RegisterBackupResources(builder *NonAdminVerbBuilder, verb string) { } // RegisterBSLResources registers bsl resource for a specific verb -// Note: BSL only supports create command, so we only register for create func RegisterBSLResources(builder *NonAdminVerbBuilder, verb string) { - // Only register BSL for create command since it doesn't have get, delete, describe, or logs - if verb == "create" { + // Only register BSL for supported verbs: create, get + if verb == "create" || verb == "get" { builder.RegisterResource("bsl", NonAdminResourceHandler{ GetCommandFunc: func(factory client.Factory) *cobra.Command { return bsl.NewBSLCommand(factory) diff --git a/cmd/non-admin/verbs/verbs.go b/cmd/non-admin/verbs/verbs.go index 167dc72..a821317 100644 --- a/cmd/non-admin/verbs/verbs.go +++ b/cmd/non-admin/verbs/verbs.go @@ -35,7 +35,13 @@ func NewGetCommand(factory client.Factory) *cobra.Command { kubectl oadp nonadmin get backup # Get a specific non-admin backup - kubectl oadp nonadmin get backup my-backup`, + kubectl oadp nonadmin get backup my-backup + + # Get all non-admin backup storage locations + kubectl oadp nonadmin get bsl + + # Get a specific backup storage location + kubectl oadp nonadmin get bsl my-storage`, }) } diff --git a/cmd/root.go b/cmd/root.go index c639384..4fd2635 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -183,23 +183,27 @@ var veleroCommandPattern = regexp.MustCompile(`(?m)(?:^|[\s\x60])velero\s+(?:` + // replaceVeleroCommandWithOADP performs context-aware replacement of "velero" with "oadp". // It only replaces "velero" when it's being used as a CLI command, not when referring to // the Velero project, server, or components. +// It also prepends "oc" or "kubectl" based on how the CLI was invoked. func replaceVeleroCommandWithOADP(text string) string { - // Replace "velero " patterns with "oadp " + // Use "oc" as the CLI prefix since OADP is primarily used on OpenShift + cliPrefix := "oc" + + // Replace "velero " patterns with "oc/kubectl oadp " result := veleroCommandPattern.ReplaceAllStringFunc(text, func(match string) string { // Preserve leading whitespace or backtick - if strings.HasPrefix(match, " ") || strings.HasPrefix(match, "\t") || strings.HasPrefix(match, "`") { + if strings.HasPrefix(match, " ") || strings.HasPrefix(match, "\t") || strings.HasPrefix(match, "`") || strings.HasPrefix(match, "\n") { prefix := match[0:1] - return prefix + strings.Replace(match[1:], "velero", "oadp", 1) + return prefix + cliPrefix + " " + strings.Replace(match[1:], "velero", "oadp", 1) } - // Start of line - just replace velero - return strings.Replace(match, "velero", "oadp", 1) + // Start of line - prepend cli prefix + return cliPrefix + " " + strings.Replace(match, "velero", "oadp", 1) }) return result } // replaceVeleroWithOADP recursively replaces all mentions of "velero" with "oadp" in the -// Example field of the given command and all its children. It also wraps the Run function -// to replace "velero" with "oadp" in runtime output. +// Example field of the given command and all its children. It also wraps the Run and RunE +// functions to replace "velero" with "oadp" in runtime output. func replaceVeleroWithOADP(cmd *cobra.Command) *cobra.Command { // Replace in multiple command fields using context-aware replacement cmd.Example = replaceVeleroCommandWithOADP(cmd.Example) @@ -231,6 +235,35 @@ func replaceVeleroWithOADP(cmd *cobra.Command) *cobra.Command { } } + // Wrap the RunE function to replace velero in output + if cmd.RunE != nil { + originalRunE := cmd.RunE + cmd.RunE = func(c *cobra.Command, args []string) error { + // Capture stdout temporarily + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + // Run the original command + err := originalRunE(c, args) + + // Restore stdout + w.Close() + os.Stdout = oldStdout + + // Read captured output and replace velero with oadp (context-aware) + var buf strings.Builder + _, copyErr := io.Copy(&buf, r) + if copyErr != nil { + fmt.Fprintf(os.Stderr, "WARNING: Error copying output: %v\n", copyErr) + } + output := replaceVeleroCommandWithOADP(buf.String()) + fmt.Print(output) + + return err + } + } + // Recursively process all child commands for _, child := range cmd.Commands() { replaceVeleroWithOADP(child) @@ -384,6 +417,12 @@ func NewVeleroRootCommand(baseName string) *cobra.Command { renameTimeoutFlag(cmd) } + // Set custom usage template to show "oc oadp" instead of just "oadp" + usageTemplate := c.UsageTemplate() + usageTemplate = strings.ReplaceAll(usageTemplate, "{{.CommandPath}}", "oc {{.CommandPath}}") + usageTemplate = strings.ReplaceAll(usageTemplate, "{{.UseLine}}", "oc {{.UseLine}}") + c.SetUsageTemplate(usageTemplate) + klog.InitFlags(flag.CommandLine) c.PersistentFlags().AddGoFlagSet(flag.CommandLine) return c diff --git a/cmd/root_test.go b/cmd/root_test.go index b2778fc..bc93d51 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -146,10 +146,10 @@ func TestReplaceVeleroWithOADP_BasicReplacement(t *testing.T) { if strings.Contains(cmd.Example, "velero") { t.Errorf("Expected 'velero' to be replaced in Example, got: %s", cmd.Example) } - if !strings.Contains(cmd.Example, "oadp") { - t.Errorf("Expected 'oadp' in Example, got: %s", cmd.Example) + if !strings.Contains(cmd.Example, "oc oadp") { + t.Errorf("Expected 'oc oadp' in Example, got: %s", cmd.Example) } - expected := "oadp backup create my-backup" + expected := "oc oadp backup create my-backup" if cmd.Example != expected { t.Errorf("Expected Example to be %q, got %q", expected, cmd.Example) } @@ -187,14 +187,14 @@ func TestReplaceVeleroWithOADP_RecursiveReplacement(t *testing.T) { } // Verify replacement happened - if !strings.Contains(parent.Example, "oadp") { - t.Errorf("Parent Example doesn't contain 'oadp': %s", parent.Example) + if !strings.Contains(parent.Example, "oc oadp") { + t.Errorf("Parent Example doesn't contain 'oc oadp': %s", parent.Example) } - if !strings.Contains(child.Example, "oadp") { - t.Errorf("Child Example doesn't contain 'oadp': %s", child.Example) + if !strings.Contains(child.Example, "oc oadp") { + t.Errorf("Child Example doesn't contain 'oc oadp': %s", child.Example) } - if !strings.Contains(grandchild.Example, "oadp") { - t.Errorf("Grandchild Example doesn't contain 'oadp': %s", grandchild.Example) + if !strings.Contains(grandchild.Example, "oc oadp") { + t.Errorf("Grandchild Example doesn't contain 'oc oadp': %s", grandchild.Example) } } @@ -213,10 +213,10 @@ Use velero backup logs to check status`, t.Errorf("Example still contains 'velero': %s", cmd.Example) } - // Count occurrences of "oadp" - count := strings.Count(cmd.Example, "oadp") + // Count occurrences of "oc oadp" + count := strings.Count(cmd.Example, "oc oadp") if count != 3 { - t.Errorf("Expected 3 occurrences of 'oadp', got %d", count) + t.Errorf("Expected 3 occurrences of 'oc oadp', got %d\nActual output:\n%s", count, cmd.Example) } } @@ -258,19 +258,19 @@ func TestReplaceVeleroWithOADP_RunFunctionWrapper(t *testing.T) { t.Error("Original Run function was not executed") } - if strings.Contains(output, "velero") { - t.Errorf("Output still contains 'velero': %s", output) + if strings.Contains(output, "velero backup") { + t.Errorf("Output still contains 'velero backup': %s", output) } - if !strings.Contains(output, "oadp") { - t.Errorf("Output doesn't contain 'oadp': %s", output) + if !strings.Contains(output, "oc oadp") { + t.Errorf("Output doesn't contain 'oc oadp': %s", output) } // Verify both lines were replaced - if !strings.Contains(output, "oadp backup describe") { + if !strings.Contains(output, "oc oadp backup describe") { t.Errorf("First line not properly replaced: %s", output) } - if !strings.Contains(output, "oadp backup logs") { + if !strings.Contains(output, "oc oadp backup logs") { t.Errorf("Second line not properly replaced: %s", output) } } @@ -351,8 +351,8 @@ func TestReplaceVeleroWithOADP_CaseSensitive(t *testing.T) { if strings.Contains(cmd.Example, "velero backup describe") { t.Errorf("Expected lowercase 'velero' to be replaced, got: %s", cmd.Example) } - if !strings.Contains(cmd.Example, "oadp backup describe") { - t.Errorf("Expected 'oadp backup describe' after replacement, got: %s", cmd.Example) + if !strings.Contains(cmd.Example, "oc oadp backup describe") { + t.Errorf("Expected 'oc oadp backup describe' after replacement, got: %s", cmd.Example) } } @@ -381,7 +381,7 @@ func TestReplaceVeleroWithOADP_PreservesProperNouns(t *testing.T) { { name: "mixed - command and reference", input: "Run velero backup create to use the velero backup feature", - expected: "Run oadp backup create to use the velero backup feature", + expected: "Run oc oadp backup create to use the velero backup feature", }, { name: "velero namespace", @@ -391,12 +391,12 @@ func TestReplaceVeleroWithOADP_PreservesProperNouns(t *testing.T) { { name: "command at start of line", input: "velero backup get my-backup", - expected: "oadp backup get my-backup", + expected: "oc oadp backup get my-backup", }, { name: "command after backtick", input: "Run `velero backup logs` for details", - expected: "Run `oadp backup logs` for details", + expected: "Run `oc oadp backup logs` for details", }, } @@ -437,7 +437,7 @@ func TestReplaceVeleroWithOADP_RunOutputPreservesProperNouns(t *testing.T) { outputFunc: func() { fmt.Println("Run `velero backup describe test` for details") }, - shouldContain: []string{"oadp backup describe"}, + shouldContain: []string{"oc oadp backup describe"}, shouldNotContain: []string{"velero backup describe"}, }, { @@ -445,8 +445,8 @@ func TestReplaceVeleroWithOADP_RunOutputPreservesProperNouns(t *testing.T) { outputFunc: func() { fmt.Println("Use velero backup create to backup using the velero backup controller") }, - shouldContain: []string{"oadp backup create", "velero backup controller"}, - shouldNotContain: []string{"velero backup create", "oadp backup controller"}, + shouldContain: []string{"oc oadp backup create", "velero backup controller"}, + shouldNotContain: []string{"velero backup create", "oc oadp backup controller"}, }, } diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go index f27f2be..68767a4 100644 --- a/internal/testutil/testutil.go +++ b/internal/testutil/testutil.go @@ -32,6 +32,8 @@ import ( const ( // TestTimeout is the default timeout for test operations TestTimeout = 30 * time.Second + // BuildTimeout is the timeout for building the CLI binary (longer for CI) + BuildTimeout = 2 * time.Minute ) // GetProjectRoot returns the root directory of the project @@ -76,8 +78,8 @@ func BuildCLIBinary(t *testing.T) string { t.Logf("Building CLI binary: %s", binaryPath) t.Logf("Project root: %s", projectRoot) - // Build the binary - ctx, cancel := context.WithTimeout(context.Background(), TestTimeout) + // Build the binary with a longer timeout for CI environments + ctx, cancel := context.WithTimeout(context.Background(), BuildTimeout) defer cancel() cmd := exec.CommandContext(ctx, "go", "build", "-o", binaryPath, ".")