Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ kubectl-oadp-linux-*
kubectl-oadp-darwin-*
kubectl-oadp-windows-*

# Local development tools
bin/

# Release artifacts
*.tar.gz
*.sha256
Expand Down
50 changes: 43 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ PLATFORM ?=
GOOS = $(word 1,$(subst /, ,$(PLATFORM)))
GOARCH = $(word 2,$(subst /, ,$(PLATFORM)))

# Helper function to get binary name with .exe for Windows
define get_binary_name
$(if $(findstring windows,$(1)),$(BINARY_NAME).exe,$(BINARY_NAME))
endef

# Default target
.PHONY: help
help: ## Show this help message
Expand Down Expand Up @@ -62,10 +57,11 @@ help: ## Show this help message
@echo " make build PLATFORM=windows/amd64"
@echo " make build PLATFORM=windows/arm64"
@echo ""
@echo "Testing commands:"
@echo "Testing and linting commands:"
@echo " make test # Run all tests (unit + integration)"
@echo " make test-unit # Run unit tests only"
@echo " make test-integration # Run integration tests only"
@echo " make lint # Run golangci-lint checks"
@echo ""
@echo "Release commands:"
@echo " make release-build # Build binaries for all platforms"
Expand Down Expand Up @@ -329,6 +325,41 @@ uninstall-all: ## Uninstall the kubectl plugin from all locations (user + system
@make --no-print-directory uninstall
@make --no-print-directory uninstall-system

# Local binary directory for development tools
LOCALBIN ?= $(shell pwd)/bin
$(LOCALBIN):
mkdir -p $(LOCALBIN)

# Tool versions
GOLANGCI_LINT_VERSION ?= v1.63.4

# Tool binaries
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint

# go-install-tool will 'go install' any package $2 and install it to $1.
define go-install-tool
[ -f $(1) ] || { \
set -e ;\
TMP_DIR=$$(mktemp -d) ;\
cd $$TMP_DIR ;\
go mod init tmp ;\
echo "Downloading $(2)" ;\
GOBIN=$(LOCALBIN) go install $(2) ;\
rm -rf $$TMP_DIR ;\
}
endef

# golangci-lint installation
.PHONY: golangci-lint
golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary
$(GOLANGCI_LINT): $(LOCALBIN)
@if [ -f $(GOLANGCI_LINT) ] && $(GOLANGCI_LINT) version 2>&1 | grep -q $(GOLANGCI_LINT_VERSION); then \
echo "golangci-lint $(GOLANGCI_LINT_VERSION) is already installed"; \
else \
echo "Installing golangci-lint $(GOLANGCI_LINT_VERSION)"; \
$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)); \
fi

# Testing targets
.PHONY: test
test: ## Run all tests
Expand All @@ -351,13 +382,18 @@ test-integration: ## Run integration tests only
go test . -v
@echo "✅ Integration tests completed!"

.PHONY: lint
lint: golangci-lint ## Run golangci-lint checks against all project's Go files
$(GOLANGCI_LINT) run ./...

# Cleanup targets
.PHONY: clean
clean: ## Remove built binaries
clean: ## Remove built binaries and downloaded tools
@echo "Cleaning up..."
@rm -f $(BINARY_NAME) $(BINARY_NAME).exe $(BINARY_NAME)-linux-* $(BINARY_NAME)-darwin-* $(BINARY_NAME)-windows-*
@rm -f *.tar.gz *.sha256
@rm -f oadp-*.yaml oadp-*.yaml.tmp
@rm -rf $(LOCALBIN)
@echo "✅ Cleanup complete!"

# Status and utility targets
Expand Down
11 changes: 7 additions & 4 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,11 @@ func replaceVeleroWithOADP(cmd *cobra.Command) *cobra.Command {
// Replace in multiple command fields using context-aware replacement
cmd.Example = replaceVeleroCommandWithOADP(cmd.Example)

// Skip wrapping logs commands to allow real-time streaming without buffering
isLogsCommand := cmd.Use == "logs" || strings.HasPrefix(cmd.Use, "logs ")

// Wrap the Run function to replace velero in output
if cmd.Run != nil {
if cmd.Run != nil && !isLogsCommand {
originalRun := cmd.Run
cmd.Run = func(c *cobra.Command, args []string) {
// Capture stdout temporarily
Expand All @@ -221,7 +224,7 @@ func replaceVeleroWithOADP(cmd *cobra.Command) *cobra.Command {
originalRun(c, args)

// Restore stdout
w.Close()
_ = w.Close()
os.Stdout = oldStdout

// Read captured output and replace velero with oadp (context-aware)
Expand All @@ -236,7 +239,7 @@ func replaceVeleroWithOADP(cmd *cobra.Command) *cobra.Command {
}

// Wrap the RunE function to replace velero in output
if cmd.RunE != nil {
if cmd.RunE != nil && !isLogsCommand {
originalRunE := cmd.RunE
cmd.RunE = func(c *cobra.Command, args []string) error {
// Capture stdout temporarily
Expand All @@ -248,7 +251,7 @@ func replaceVeleroWithOADP(cmd *cobra.Command) *cobra.Command {
err := originalRunE(c, args)

// Restore stdout
w.Close()
_ = w.Close()
os.Stdout = oldStdout

// Read captured output and replace velero with oadp (context-aware)
Expand Down
148 changes: 146 additions & 2 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ func TestReplaceVeleroWithOADP_RunFunctionWrapper(t *testing.T) {
cmd.Run(cmd, []string{})

// Restore stdout
w.Close()
_ = w.Close()
os.Stdout = oldStdout

// Read captured output
Expand Down Expand Up @@ -470,7 +470,7 @@ func TestReplaceVeleroWithOADP_RunOutputPreservesProperNouns(t *testing.T) {
cmd.Run(cmd, []string{})

// Restore stdout
w.Close()
_ = w.Close()
os.Stdout = oldStdout

// Read captured output
Expand Down Expand Up @@ -591,6 +591,150 @@ func TestApplyTimeoutToConfig(t *testing.T) {
}
}

// TestReplaceVeleroWithOADP_LogsCommandNotWrapped tests that logs commands are never wrapped
func TestReplaceVeleroWithOADP_LogsCommandNotWrapped(t *testing.T) {
tests := []struct {
name string
use string
shouldWrap bool
}{
{
name: "logs command",
use: "logs",
shouldWrap: false,
},
{
name: "logs with args",
use: "logs NAME",
shouldWrap: false,
},
{
name: "get command",
use: "get",
shouldWrap: true,
},
{
name: "describe command",
use: "describe",
shouldWrap: true,
},
{
name: "create command",
use: "create",
shouldWrap: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test with Run function
runCalled := false
cmd := &cobra.Command{
Use: tt.use,
Run: func(c *cobra.Command, args []string) {
runCalled = true
fmt.Println("test output with velero backup create")
},
}

// Store reference to original Run function
originalRun := cmd.Run

replaceVeleroWithOADP(cmd)

// If logs command, Run should not be wrapped (same function pointer)
// If not logs, Run should be wrapped (different function pointer)
isWrapped := fmt.Sprintf("%p", originalRun) != fmt.Sprintf("%p", cmd.Run)

if tt.shouldWrap && !isWrapped {
t.Errorf("Expected command %q to be wrapped, but it wasn't", tt.use)
}
if !tt.shouldWrap && isWrapped {
t.Errorf("Expected command %q NOT to be wrapped, but it was", tt.use)
}

// Verify the command still executes
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
cmd.Run(cmd, []string{})
_ = w.Close()
os.Stdout = oldStdout

var buf bytes.Buffer
if _, err := io.Copy(&buf, r); err != nil {
t.Fatalf("Error copying output: %v", err)
}

if !runCalled {
t.Error("Original Run function was not called")
}

output := buf.String()
if tt.shouldWrap {
// Wrapped commands should have output replaced
if strings.Contains(output, "velero backup create") {
t.Errorf("Wrapped command output should have 'velero' replaced, got: %s", output)
}
} else {
// Logs commands should NOT have output replaced
if !strings.Contains(output, "velero backup create") {
t.Errorf("Logs command output should NOT be modified, got: %s", output)
}
}
})
}

// Test with RunE function
t.Run("logs_command_runE_not_wrapped", func(t *testing.T) {
runECalled := false
cmd := &cobra.Command{
Use: "logs",
RunE: func(c *cobra.Command, args []string) error {
runECalled = true
fmt.Println("test output with velero backup logs")
return nil
},
}

originalRunE := cmd.RunE
replaceVeleroWithOADP(cmd)

// Logs command should not be wrapped
isWrapped := fmt.Sprintf("%p", originalRunE) != fmt.Sprintf("%p", cmd.RunE)
if isWrapped {
t.Error("Expected logs command RunE NOT to be wrapped, but it was")
}

// Verify output is not modified
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
err := cmd.RunE(cmd, []string{})
_ = w.Close()
os.Stdout = oldStdout

if err != nil {
t.Errorf("RunE returned error: %v", err)
}

if !runECalled {
t.Error("Original RunE function was not called")
}

var buf bytes.Buffer
if _, err := io.Copy(&buf, r); err != nil {
t.Fatalf("Error copying output: %v", err)
}
output := buf.String()

// Logs command output should NOT be modified
if !strings.Contains(output, "velero backup logs") {
t.Errorf("Logs command output should NOT be modified, got: %s", output)
}
})
}

// TestApplyTimeoutToConfig_DialerTimeout tests that the custom dialer respects the timeout
func TestApplyTimeoutToConfig_DialerTimeout(t *testing.T) {
// Set a very short timeout
Expand Down
Loading