diff --git a/Makefile b/Makefile index e36610e..d0c0867 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,13 @@ VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev VELERO_NAMESPACE ?= openshift-adp ASSUME_DEFAULT ?= false +# Build information for version command +GIT_SHA := $(shell git rev-parse HEAD 2>/dev/null || echo "unknown") +GIT_TREE_STATE := $(shell if [ -z "`git status --porcelain 2>/dev/null`" ]; then echo "clean"; else echo "dirty"; fi) +LDFLAGS := -X github.com/vmware-tanzu/velero/pkg/buildinfo.Version=$(VERSION) \ + -X github.com/vmware-tanzu/velero/pkg/buildinfo.GitSHA=$(GIT_SHA) \ + -X github.com/vmware-tanzu/velero/pkg/buildinfo.GitTreeState=$(GIT_TREE_STATE) + # Centralized platform definitions to avoid duplication # Matches architectures supported by Kubernetes: https://kubernetes.io/releases/download/#binaries PLATFORMS = linux/amd64 linux/arm64 linux/ppc64le linux/s390x darwin/amd64 darwin/arm64 windows/amd64 windows/arm64 @@ -74,7 +81,7 @@ build: ## Build the kubectl plugin binary (use PLATFORM=os/arch for cross-compil binary_suffix=""; \ fi; \ echo "Building $(BINARY_NAME) for $(PLATFORM)..."; \ - GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $(BINARY_NAME)-$(GOOS)-$(GOARCH)$$binary_suffix .; \ + GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags "$(LDFLAGS)" -o $(BINARY_NAME)-$(GOOS)-$(GOARCH)$$binary_suffix .; \ echo "✅ Built $(BINARY_NAME)-$(GOOS)-$(GOARCH)$$binary_suffix successfully!"; \ else \ GOOS=$$(go env GOOS); \ @@ -84,7 +91,7 @@ build: ## Build the kubectl plugin binary (use PLATFORM=os/arch for cross-compil binary_name="$(BINARY_NAME)"; \ fi; \ echo "Building $$binary_name for current platform ($$GOOS/$$(go env GOARCH))..."; \ - go build -o $$binary_name .; \ + go build -ldflags "$(LDFLAGS)" -o $$binary_name .; \ echo "✅ Built $$binary_name successfully!"; \ fi @@ -111,48 +118,48 @@ install: build ## Build and install the kubectl plugin to ~/.local/bin (no sudo if [[ ":$$PATH:" != *":$(INSTALL_PATH):"* ]]; then \ PATH_NEEDS_UPDATE=true; \ CURRENT_SESSION_NEEDS_UPDATE=true; \ - echo "⚠️ $(INSTALL_PATH) is not in your current PATH"; \ + echo " ├─ ⚠️ $(INSTALL_PATH) is not in your current PATH"; \ \ if [[ "$$SHELL" == */zsh* ]] && [[ -f "$$HOME/.zshrc" ]]; then \ if ! grep -q '^[[:space:]]*export[[:space:]]*PATH.*\.local/bin' "$$HOME/.zshrc" 2>/dev/null; then \ echo 'export PATH="$$HOME/.local/bin:$$PATH"' >> "$$HOME/.zshrc"; \ - echo "✅ Added PATH export to ~/.zshrc"; \ + echo " ├─ ✅ Added PATH export to ~/.zshrc"; \ PATH_UPDATED=true; \ else \ - echo "ℹ️ PATH export already exists in ~/.zshrc"; \ + echo " ├─ ℹ️ PATH export already exists in ~/.zshrc"; \ PATH_IN_CONFIG=true; \ fi; \ elif [[ "$$SHELL" == */bash* ]] && [[ -f "$$HOME/.bashrc" ]]; then \ if ! grep -q '^[[:space:]]*export[[:space:]]*PATH.*\.local/bin' "$$HOME/.bashrc" 2>/dev/null; then \ echo 'export PATH="$$HOME/.local/bin:$$PATH"' >> "$$HOME/.bashrc"; \ - echo "✅ Added PATH export to ~/.bashrc"; \ + echo " ├─ ✅ Added PATH export to ~/.bashrc"; \ PATH_UPDATED=true; \ else \ - echo "ℹ️ PATH export already exists in ~/.bashrc"; \ + echo " ├─ ℹ️ PATH export already exists in ~/.bashrc"; \ PATH_IN_CONFIG=true; \ fi; \ else \ - echo "⚠️ Unsupported shell or config file not found"; \ - echo " Manually add to your shell config: export PATH=\"$(INSTALL_PATH):$$PATH\""; \ + echo " ├─ ⚠️ Unsupported shell or config file not found"; \ + echo " │ └─ Manually add to your shell config: export PATH=\"$(INSTALL_PATH):$$PATH\""; \ PATH_UPDATED=true; \ fi; \ else \ - echo "✅ $(INSTALL_PATH) is already in PATH"; \ + echo " └─ ✅ $(INSTALL_PATH) is already in PATH"; \ fi; \ \ echo ""; \ if [[ "$$CURRENT_SESSION_NEEDS_UPDATE" == "true" ]]; then \ echo "🔧 To use kubectl oadp in this terminal session:"; \ - echo " export PATH=\"$(INSTALL_PATH):$$PATH\""; \ + echo " └─ export PATH=\"$(INSTALL_PATH):$$PATH\""; \ echo ""; \ echo "🔄 For future sessions:"; \ if [[ "$$PATH_UPDATED" == "true" ]]; then \ - echo " Restart your terminal or run: source ~/.zshrc"; \ + echo " └─ Restart your terminal or run: source ~/.zshrc"; \ elif [[ "$$PATH_IN_CONFIG" == "true" ]]; then \ - echo " Restart your terminal or run: source ~/.zshrc"; \ - echo " (PATH export exists but may need shell restart)"; \ + echo " ├─ Restart your terminal or run: source ~/.zshrc"; \ + echo " └─ (PATH export exists but may need shell restart)"; \ else \ - echo " Add the PATH export to your shell configuration file"; \ + echo " └─ Add the PATH export to your shell configuration file"; \ fi; \ fi; \ echo ""; \ @@ -161,38 +168,46 @@ install: build ## Build and install the kubectl plugin to ~/.local/bin (no sudo DETECTED=false; \ if [[ "$(ASSUME_DEFAULT)" != "true" && "$(VELERO_NAMESPACE)" == "openshift-adp" ]]; then \ echo ""; \ - echo "🔍 Detecting OADP deployment in cluster..."; \ + echo " 🔍 Detecting OADP deployment in cluster..."; \ DETECTED_NS=$$(kubectl get deployments --all-namespaces -o jsonpath='{.items[?(@.metadata.name=="openshift-adp-controller-manager")].metadata.namespace}' 2>/dev/null | head -1); \ if [[ -n "$$DETECTED_NS" ]]; then \ - echo "✅ Found OADP controller in namespace: $$DETECTED_NS"; \ + echo " ├─ ✅ Found OADP controller in namespace: $$DETECTED_NS"; \ NAMESPACE=$$DETECTED_NS; \ DETECTED=true; \ else \ - echo " Could not find openshift-adp-controller-manager deployment"; \ + echo " ├─ ⚠️ Could not find openshift-adp-controller-manager deployment"; \ fi; \ - echo "🔍 Looking for DataProtectionApplication (DPA) resources..."; \ + echo ""; \ + echo " 🔍 Looking for DataProtectionApplication (DPA) resources..."; \ DETECTED_NS=$$(kubectl get dataprotectionapplication --all-namespaces -o jsonpath='{.items[0].metadata.namespace}' 2>/dev/null | head -1); \ if [[ -n "$$DETECTED_NS" ]]; then \ - echo "✅ Found DPA resource in namespace: $$DETECTED_NS"; \ + echo " ├─ ✅ Found DPA resource in namespace: $$DETECTED_NS"; \ NAMESPACE=$$DETECTED_NS; \ DETECTED=true; \ else \ - echo " Could not find DataProtectionApplication resources"; \ + echo " ├─ ⚠️ Could not find DataProtectionApplication resources"; \ fi; \ - echo "🔍 Looking for Velero deployment as fallback..."; \ + echo ""; \ + echo " ⚠️ ⚠️ ⚠️"; \ + echo " ├─ ❌ OADP Operator is not detected in the cluster"; \ + echo " ├─ Fallback will check for Velero deployment as fallback"; \ + echo " ├─ Consider using the velero cli instead"; \ + echo " ⚠️ ⚠️ ⚠️"; \ + echo ""; \ + echo " 🔍 Looking for Velero deployment as fallback..."; \ DETECTED_NS=$$(kubectl get deployments --all-namespaces -o jsonpath='{.items[?(@.metadata.name=="velero")].metadata.namespace}' 2>/dev/null | head -1); \ if [[ -n "$$DETECTED_NS" ]]; then \ - echo "✅ Found Velero deployment in namespace: $$DETECTED_NS"; \ + echo " ├─ ✅ Found Velero deployment in namespace: $$DETECTED_NS"; \ NAMESPACE=$$DETECTED_NS; \ DETECTED=true; \ else \ - echo "⚠️ Could not detect OADP or Velero deployment in cluster"; \ + echo " └─ ⚠️ Could not detect OADP or Velero deployment in cluster"; \ fi; \ if [[ "$$DETECTED" == "false" ]]; then \ - echo "🤔 Which namespace should admin commands use for Velero resources?"; \ - echo " (Common options: openshift-adp, velero, oadp)"; \ + echo " 🤔 Which namespace should admin commands use for Velero resources?"; \ + echo " │ └─ (Common options: openshift-adp, velero, oadp)"; \ echo ""; \ - printf "Enter namespace [default: $(VELERO_NAMESPACE)]: "; \ + printf " Enter namespace [default: $(VELERO_NAMESPACE)]: "; \ read -r user_input; \ if [[ -n "$$user_input" ]]; then \ NAMESPACE=$$user_input; \ @@ -200,7 +215,7 @@ install: build ## Build and install the kubectl plugin to ~/.local/bin (no sudo fi; \ echo ""; \ fi; \ - echo "Setting Velero namespace to: $$NAMESPACE"; \ + echo " ├─ Setting Velero namespace to: $$NAMESPACE"; \ GOOS=$$(go env GOOS); \ if [ "$$GOOS" = "windows" ]; then \ binary_name="$(BINARY_NAME).exe"; \ @@ -208,42 +223,38 @@ install: build ## Build and install the kubectl plugin to ~/.local/bin (no sudo binary_name="$(BINARY_NAME)"; \ fi; \ $(INSTALL_PATH)/$$binary_name client config set namespace=$$NAMESPACE 2>/dev/null || true; \ - echo "✅ Client config initialized"; \ + echo " └─ ✅ Client config initialized"; \ echo ""; \ echo "🧪 Verifying installation..."; \ if [[ "$$CURRENT_SESSION_NEEDS_UPDATE" == "true" ]]; then \ - echo " Temporarily updating PATH for verification"; \ + echo " ├─ Temporarily updating PATH for verification"; \ if PATH="$(INSTALL_PATH):$$PATH" command -v kubectl >/dev/null 2>&1; then \ if PATH="$(INSTALL_PATH):$$PATH" kubectl plugin list 2>/dev/null | grep -q "kubectl-oadp"; then \ - echo "✅ Installation verified: kubectl oadp plugin is accessible"; \ - PATH="$(INSTALL_PATH):$$PATH" kubectl oadp version 2>/dev/null || echo " (Note: version command requires cluster access)"; \ + echo " ├─ ✅ Installation verified: kubectl oadp plugin is accessible"; \ + PATH="$(INSTALL_PATH):$$PATH" kubectl oadp version 2>/dev/null || echo " │ └─ (Note: version command requires cluster access)"; \ else \ - echo "❌ Installation verification failed: kubectl oadp plugin not found"; \ - echo " Try running: export PATH=\"$(INSTALL_PATH):$$PATH\""; \ + echo " ├─ ❌ Installation verification failed: kubectl oadp plugin not found"; \ + echo " │ └─ Try running: export PATH=\"$(INSTALL_PATH):$$PATH\""; \ fi; \ else \ - echo "⚠️ kubectl not found - cannot verify plugin accessibility"; \ - echo " Plugin installed to: $(INSTALL_PATH)/$$binary_name"; \ + echo " ├─ ⚠️ kubectl not found - cannot verify plugin accessibility"; \ + echo " └─ Plugin installed to: $(INSTALL_PATH)/$$binary_name"; \ fi; \ else \ if command -v kubectl >/dev/null 2>&1; then \ if kubectl plugin list 2>/dev/null | grep -q "kubectl-oadp"; then \ - echo "✅ Installation verified: kubectl oadp plugin is accessible"; \ - kubectl oadp version 2>/dev/null || echo " (Note: version command requires cluster access)"; \ + echo " ├─ ✅ Installation verified: kubectl oadp plugin is accessible"; \ + echo " ├─ Running version command..."; \ + echo ""; \ + kubectl oadp version 2>/dev/null || echo " │ └─ (Note: version command requires cluster access)"; \ else \ - echo "❌ Installation verification failed: kubectl oadp plugin not found"; \ + echo " └─ ❌ Installation verification failed: kubectl oadp plugin not found"; \ fi; \ else \ - echo "⚠️ kubectl not found - cannot verify plugin accessibility"; \ - echo " Plugin installed to: $(INSTALL_PATH)/$$binary_name"; \ + echo " ├─ ⚠️ kubectl not found - cannot verify plugin accessibility"; \ + echo " └─ Plugin installed to: $(INSTALL_PATH)/$$binary_name"; \ fi; \ fi; \ - echo ""; \ - echo "📋 Next steps:"; \ - echo " 1. Test admin commands: kubectl oadp backup get"; \ - echo " 2. Test non-admin commands: kubectl oadp nonadmin backup get"; \ - echo " 3. Manage NABSL requests: kubectl oadp nabsl get"; \ - echo " 4. Change namespace: kubectl oadp client config set namespace=" .PHONY: install-user install-user: build ## Build and install the kubectl plugin to ~/.local/bin (no sudo required) @@ -390,7 +401,7 @@ release-build: ## Build binaries for all platforms output_name="$(BINARY_NAME)$${version_suffix}_$${GOOS}_$${GOARCH}"; \ fi; \ echo "Building $$output_name..."; \ - GOOS=$$GOOS GOARCH=$$GOARCH go build -o $$output_name .; \ + GOOS=$$GOOS GOARCH=$$GOARCH go build -ldflags "$(LDFLAGS)" -o $$output_name .; \ echo "✅ Built $$output_name"; \ done @echo "✅ All release binaries created successfully!" diff --git a/cmd/root.go b/cmd/root.go index d9f3bc2..7acc04b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -19,7 +19,10 @@ package cmd import ( "flag" "fmt" + "io" "os" + "regexp" + "strings" "github.com/fatih/color" "github.com/migtools/oadp-cli/cmd/nabsl-request" @@ -50,6 +53,76 @@ import ( "sigs.k8s.io/kustomize/cmd/config/completion" ) +// veleroCommandPattern matches "velero" when used as a CLI command. +// It matches "velero" followed by common command patterns, including two-word commands +// like "backup create", "restore get", etc. +var veleroCommandPattern = regexp.MustCompile(`(?m)(?:^|[\s\x60])velero\s+(?:` + + // Two-word commands: "backup create", "restore get", etc. + `(?:backup|restore|schedule)\s+(?:create|get|delete|describe|logs|download|patch)` + + `|` + + // Single-word commands + `(?:version|install|uninstall|plugin|snapshot-location|backup-location|restic|repo|client|completion|bug|debug|datamover)` + + `)`) + +// 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. +func replaceVeleroCommandWithOADP(text string) string { + // Replace "velero " patterns with "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, "`") { + prefix := match[0:1] + return prefix + strings.Replace(match[1:], "velero", "oadp", 1) + } + // Start of line - just replace velero + return 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. +func replaceVeleroWithOADP(cmd *cobra.Command) *cobra.Command { + // Replace in multiple command fields using context-aware replacement + cmd.Example = replaceVeleroCommandWithOADP(cmd.Example) + + // Wrap the Run function to replace velero in output + if cmd.Run != nil { + originalRun := cmd.Run + cmd.Run = func(c *cobra.Command, args []string) { + // Capture stdout temporarily + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + // Run the original command + originalRun(c, args) + + // Restore stdout + w.Close() + os.Stdout = oldStdout + + // Read captured output and replace velero with oadp (context-aware) + var buf strings.Builder + _, err := io.Copy(&buf, r) + if err != nil { + fmt.Fprintf(os.Stderr, "WARNING: Error copying output: %v\n", err) + } + output := replaceVeleroCommandWithOADP(buf.String()) + fmt.Print(output) + } + } + + // Recursively process all child commands + for _, child := range cmd.Commands() { + replaceVeleroWithOADP(child) + } + + return cmd +} + // NewVeleroRootCommand returns a root command with all Velero CLI subcommands attached. func NewVeleroRootCommand(baseName string) *cobra.Command { @@ -113,6 +186,11 @@ func NewVeleroRootCommand(baseName string) *cobra.Command { // Custom subcommands - use NonAdmin factory c.AddCommand(nonadmin.NewNonAdminCommand(f)) + // Apply velero->oadp replacement to all commands recursively + for _, cmd := range c.Commands() { + replaceVeleroWithOADP(cmd) + } + klog.InitFlags(flag.CommandLine) c.PersistentFlags().AddGoFlagSet(flag.CommandLine) return c diff --git a/cmd/root_test.go b/cmd/root_test.go index d388039..0a72483 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -17,9 +17,15 @@ limitations under the License. package cmd import ( + "bytes" + "fmt" + "io" + "os" + "strings" "testing" "github.com/migtools/oadp-cli/internal/testutil" + "github.com/spf13/cobra" ) // TestRootCommand tests the root command functionality @@ -123,3 +129,365 @@ func TestRootCommandSmoke(t *testing.T) { }) } } + +// TestReplaceVeleroWithOADP_BasicReplacement tests basic Example field replacement +func TestReplaceVeleroWithOADP_BasicReplacement(t *testing.T) { + cmd := &cobra.Command{ + Use: "test", + Example: "velero backup create my-backup", + } + + replaceVeleroWithOADP(cmd) + + 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) + } + expected := "oadp backup create my-backup" + if cmd.Example != expected { + t.Errorf("Expected Example to be %q, got %q", expected, cmd.Example) + } +} + +// TestReplaceVeleroWithOADP_RecursiveReplacement tests recursive child command replacement +func TestReplaceVeleroWithOADP_RecursiveReplacement(t *testing.T) { + parent := &cobra.Command{ + Use: "parent", + Example: "velero backup get", + } + child := &cobra.Command{ + Use: "child", + Example: "velero backup create test", + } + grandchild := &cobra.Command{ + Use: "grandchild", + Example: "velero restore describe my-restore", + } + + child.AddCommand(grandchild) + parent.AddCommand(child) + + replaceVeleroWithOADP(parent) + + // Check all levels were replaced + if strings.Contains(parent.Example, "velero") { + t.Errorf("Parent Example still contains 'velero': %s", parent.Example) + } + if strings.Contains(child.Example, "velero") { + t.Errorf("Child Example still contains 'velero': %s", child.Example) + } + if strings.Contains(grandchild.Example, "velero") { + t.Errorf("Grandchild Example still contains 'velero': %s", grandchild.Example) + } + + // Verify replacement happened + if !strings.Contains(parent.Example, "oadp") { + t.Errorf("Parent Example doesn't contain 'oadp': %s", parent.Example) + } + if !strings.Contains(child.Example, "oadp") { + t.Errorf("Child Example doesn't contain 'oadp': %s", child.Example) + } + if !strings.Contains(grandchild.Example, "oadp") { + t.Errorf("Grandchild Example doesn't contain 'oadp': %s", grandchild.Example) + } +} + +// TestReplaceVeleroWithOADP_MultipleOccurrences tests replacing multiple occurrences +func TestReplaceVeleroWithOADP_MultipleOccurrences(t *testing.T) { + cmd := &cobra.Command{ + Use: "test", + Example: `velero backup create my-backup +velero backup get my-backup +Use velero backup logs to check status`, + } + + replaceVeleroWithOADP(cmd) + + if strings.Contains(cmd.Example, "velero") { + t.Errorf("Example still contains 'velero': %s", cmd.Example) + } + + // Count occurrences of "oadp" + count := strings.Count(cmd.Example, "oadp") + if count != 3 { + t.Errorf("Expected 3 occurrences of 'oadp', got %d", count) + } +} + +// TestReplaceVeleroWithOADP_RunFunctionWrapper tests stdout capture and replacement +func TestReplaceVeleroWithOADP_RunFunctionWrapper(t *testing.T) { + outputCaptured := false + cmd := &cobra.Command{ + Use: "test", + Run: func(c *cobra.Command, args []string) { + fmt.Println("Run `velero backup describe test` for details.") + fmt.Println("Or use velero backup logs test") + outputCaptured = true + }, + } + + replaceVeleroWithOADP(cmd) + + // Capture stdout + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + // Run the wrapped command + cmd.Run(cmd, []string{}) + + // Restore stdout + w.Close() + os.Stdout = oldStdout + + // Read captured output + var buf bytes.Buffer + _, err := io.Copy(&buf, r) + if err != nil { + t.Errorf("Error copying output: %v", err) + } + output := buf.String() + + if !outputCaptured { + 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, "oadp") { + t.Errorf("Output doesn't contain 'oadp': %s", output) + } + + // Verify both lines were replaced + if !strings.Contains(output, "oadp backup describe") { + t.Errorf("First line not properly replaced: %s", output) + } + if !strings.Contains(output, "oadp backup logs") { + t.Errorf("Second line not properly replaced: %s", output) + } +} + +// TestReplaceVeleroWithOADP_EmptyFields tests handling of empty fields +func TestReplaceVeleroWithOADP_EmptyFields(t *testing.T) { + cmd := &cobra.Command{ + Use: "test", + Example: "", + } + + // Should not panic + replaceVeleroWithOADP(cmd) + + if cmd.Example != "" { + t.Errorf("Expected empty Example to remain empty, got: %s", cmd.Example) + } +} + +// TestReplaceVeleroWithOADP_NilRun tests handling of nil Run function +func TestReplaceVeleroWithOADP_NilRun(t *testing.T) { + cmd := &cobra.Command{ + Use: "test", + Example: "velero test", + Run: nil, + } + + // Should not panic + replaceVeleroWithOADP(cmd) + + if cmd.Run != nil { + t.Error("Expected Run to remain nil") + } +} + +// TestReplaceVeleroWithOADP_PreservesOtherFields tests that other fields are not affected +func TestReplaceVeleroWithOADP_PreservesOtherFields(t *testing.T) { + originalShort := "Short description" + originalLong := "Long description" + originalUse := "test-command" + + cmd := &cobra.Command{ + Use: originalUse, + Short: originalShort, + Long: originalLong, + Example: "velero backup create", + } + + replaceVeleroWithOADP(cmd) + + if cmd.Use != originalUse { + t.Errorf("Use field was modified: expected %q, got %q", originalUse, cmd.Use) + } + if cmd.Short != originalShort { + t.Errorf("Short field was modified: expected %q, got %q", originalShort, cmd.Short) + } + if cmd.Long != originalLong { + t.Errorf("Long field was modified: expected %q, got %q", originalLong, cmd.Long) + } +} + +// TestReplaceVeleroWithOADP_CaseSensitive tests that replacement is case-sensitive +func TestReplaceVeleroWithOADP_CaseSensitive(t *testing.T) { + cmd := &cobra.Command{ + Use: "test", + Example: "Velero backup create\nVELERO backup get\nvelero backup describe", + } + + replaceVeleroWithOADP(cmd) + + // Only lowercase "velero" should be replaced + if !strings.Contains(cmd.Example, "Velero") { + t.Errorf("Expected 'Velero' (capitalized) to remain, got: %s", cmd.Example) + } + if !strings.Contains(cmd.Example, "VELERO") { + t.Errorf("Expected 'VELERO' (uppercase) to remain, got: %s", cmd.Example) + } + 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) + } +} + +// TestReplaceVeleroWithOADP_PreservesProperNouns tests that "velero" referring to the project is preserved +func TestReplaceVeleroWithOADP_PreservesProperNouns(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "velero server reference", + input: "This starts the velero server", + expected: "This starts the velero server", + }, + { + name: "about velero", + input: "Learn more about velero at velero.io", + expected: "Learn more about velero at velero.io", + }, + { + name: "velero project", + input: "The velero project provides backup capabilities", + expected: "The velero project provides backup capabilities", + }, + { + 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", + }, + { + name: "velero namespace", + input: "Resources are in the velero namespace", + expected: "Resources are in the velero namespace", + }, + { + name: "command at start of line", + input: "velero backup get my-backup", + expected: "oadp backup get my-backup", + }, + { + name: "command after backtick", + input: "Run `velero backup logs` for details", + expected: "Run `oadp backup logs` for details", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := &cobra.Command{ + Use: "test", + Example: tt.input, + } + + replaceVeleroWithOADP(cmd) + + if cmd.Example != tt.expected { + t.Errorf("Expected: %q\nGot: %q", tt.expected, cmd.Example) + } + }) + } +} + +// TestReplaceVeleroWithOADP_RunOutputPreservesProperNouns tests Run wrapper preserves "velero" references +func TestReplaceVeleroWithOADP_RunOutputPreservesProperNouns(t *testing.T) { + tests := []struct { + name string + outputFunc func() + shouldContain []string + shouldNotContain []string + }{ + { + name: "server reference preserved", + outputFunc: func() { + fmt.Println("The velero server is running") + }, + shouldContain: []string{"velero server"}, + shouldNotContain: []string{"oadp server"}, + }, + { + name: "command replaced", + outputFunc: func() { + fmt.Println("Run `velero backup describe test` for details") + }, + shouldContain: []string{"oadp backup describe"}, + shouldNotContain: []string{"velero backup describe"}, + }, + { + name: "mixed content", + 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"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := &cobra.Command{ + Use: "test", + Run: func(c *cobra.Command, args []string) { + tt.outputFunc() + }, + } + + replaceVeleroWithOADP(cmd) + + // Capture stdout + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + // Run the wrapped command + cmd.Run(cmd, []string{}) + + // Restore stdout + w.Close() + os.Stdout = oldStdout + + // Read captured output + var buf bytes.Buffer + _, err := io.Copy(&buf, r) + if err != nil { + t.Errorf("Error copying output: %v", err) + } + output := buf.String() + + for _, should := range tt.shouldContain { + if !strings.Contains(output, should) { + t.Errorf("Expected output to contain %q, got: %s", should, output) + } + } + + for _, shouldNot := range tt.shouldNotContain { + if strings.Contains(output, shouldNot) { + t.Errorf("Expected output NOT to contain %q, got: %s", shouldNot, output) + } + } + }) + } +} diff --git a/main.go b/main.go index 84e946f..b89c4a3 100644 --- a/main.go +++ b/main.go @@ -17,9 +17,6 @@ limitations under the License. package main import ( - "os" - "path/filepath" - "github.com/migtools/oadp-cli/cmd" velerocmd "github.com/vmware-tanzu/velero/pkg/cmd" _ "k8s.io/client-go/plugin/pkg/client/auth" @@ -28,8 +25,7 @@ import ( func main() { defer klog.Flush() - baseName := filepath.Base(os.Args[0]) - err := cmd.NewVeleroRootCommand(baseName).Execute() + err := cmd.NewVeleroRootCommand("oadp").Execute() velerocmd.CheckError(err) }