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 @@ -12,6 +12,9 @@ kubectl-oadp-linux-*
kubectl-oadp-darwin-*
kubectl-oadp-windows-*

# Must-gather artifacts
/must-gather/

# Local development tools
bin/

Expand Down
16 changes: 8 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,8 @@ install: build ## Build and install the kubectl plugin to ~/.local/bin (no sudo
echo " export PATH=\"$(INSTALL_PATH):$$PATH\""; \
echo ""; \
echo " Quick start:"; \
echo " • kubectl oadp --help # Show available commands"; \
echo " • kubectl oadp backup get # List backups"; \
echo " • oc oadp --help # Show available commands"; \
echo " • oc oadp backup get # List backups"; \
else \
echo " ├─ ❌ Installation verification failed: kubectl oadp plugin not found"; \
echo " │ └─ Try running: export PATH=\"$(INSTALL_PATH):$$PATH\""; \
Expand All @@ -265,9 +265,9 @@ install: build ## Build and install the kubectl plugin to ~/.local/bin (no sudo
echo "🎉 Installation complete!"; \
echo ""; \
echo " Quick start:"; \
echo " • kubectl oadp --help # Show available commands"; \
echo " • kubectl oadp backup get # List backups"; \
echo " • kubectl oadp version # Show version info"; \
echo " • oc oadp --help # Show available commands"; \
echo " • oc oadp backup get # List backups"; \
echo " • oc oadp version # Show version info"; \
else \
echo " └─ ❌ Installation verification failed: kubectl oadp plugin not found"; \
fi; \
Expand All @@ -284,7 +284,7 @@ install-user: build ## Build and install the kubectl plugin to ~/.local/bin (no
cp $(BINARY_NAME) ~/.local/bin/
@echo "✅ Installed to ~/.local/bin"
@echo "Add to PATH: export PATH=\"\$$HOME/.local/bin:\$$PATH\""
@echo "Test: kubectl oadp --help"
@echo "Test: oc oadp --help"

.PHONY: install-bin
install-bin: build ## Build and install the kubectl plugin to ~/bin (no sudo required)
Expand All @@ -293,14 +293,14 @@ install-bin: build ## Build and install the kubectl plugin to ~/bin (no sudo req
cp $(BINARY_NAME) ~/bin/
@echo "✅ Installed to ~/bin"
@echo "Add to PATH: export PATH=\"\$$HOME/bin:\$$PATH\""
@echo "Test: kubectl oadp --help"
@echo "Test: oc oadp --help"

.PHONY: install-system
install-system: build ## Build and install the kubectl plugin to /usr/local/bin (requires sudo)
@echo "Installing $(BINARY_NAME) to /usr/local/bin..."
@sudo mv $(BINARY_NAME) /usr/local/bin/
@echo "✅ Installed to /usr/local/bin"
@echo "Test: kubectl oadp --help"
@echo "Test: oc oadp --help"

.PHONY: uninstall
uninstall: ## Uninstall the kubectl plugin from user locations
Expand Down
186 changes: 186 additions & 0 deletions cmd/must-gather/must_gather.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/*
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 mustgather

import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"time"

"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/vmware-tanzu/velero/pkg/client"
)

// MustGatherOptions holds the options for the must-gather command
type MustGatherOptions struct {
DestDir string
RequestTimeout time.Duration
SkipTLS bool
Image string

// Internal state
effectiveImage string // Resolved image (after version detection or default)
}

// BindFlags binds the flags to the command
func (o *MustGatherOptions) BindFlags(flags *pflag.FlagSet) {
flags.StringVar(&o.DestDir, "dest-dir", "", "Directory where must-gather output will be stored (defaults to current directory)")
flags.DurationVar(&o.RequestTimeout, "request-timeout", 0, "Timeout for the gather script (e.g., '1m', '30s')")
flags.BoolVar(&o.SkipTLS, "skip-tls", false, "Skip TLS verification")
flags.StringVar(&o.Image, "image", "", "Must-gather image to use (defaults to OADP must-gather image)")
_ = flags.MarkHidden("image") // Hidden flag for advanced users
}

// Complete completes the options
func (o *MustGatherOptions) Complete(args []string, f client.Factory) error {
// Determine effective image to use
// For v1: Use hardcoded default if --image not specified
if o.Image == "" {
o.effectiveImage = "registry.redhat.io/oadp/oadp-mustgather-rhel9:v1.5"
// TODO: Future enhancement - detect version and map to image
// o.effectiveImage = o.getDefaultImage()
Comment on lines +58 to +59
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai open issue to follow up.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Create a GitHub issue for the TODO follow-up
gh issue create \
  --repo migtools/oadp-cli \
  --title "Implement version detection for must-gather image selection" \
  --body "## Description

Currently, the must-gather command uses a hardcoded default image when the \`--image\` flag is not specified:

\`\`\`go
o.effectiveImage = \"registry.redhat.io/oadp/oadp-mustgather-rhel9:v1.5\"
\`\`\`

As noted in the TODO comment in \`cmd/must-gather/must_gather.go\`, a future enhancement should automatically detect the installed OADP version and map it to the appropriate must-gather image.

## Proposed Enhancement

- Detect the OADP operator version installed in the cluster
- Maintain a mapping of OADP versions to their corresponding must-gather images
- Automatically select the correct must-gather image based on the detected version
- Fall back to a reasonable default if version detection fails

## References

- PR: https://github.com/migtools/oadp-cli/pull/115
- Comment: https://github.com/migtools/oadp-cli/pull/115#discussion_r2788814569
- Requested by: `@kaovilai`" \
  --assignee kaovilai

Length of output: 47


@kaovilai I've created issue #117 to track the enhancement for automatic OADP version detection and must-gather image mapping. The issue has been assigned to you and includes references back to this PR and comment.

} else {
o.effectiveImage = o.Image
}

return nil
}

// Validate validates the options
func (o *MustGatherOptions) Validate(c *cobra.Command, args []string, f client.Factory) error {
// Verify oc command exists
if _, err := exec.LookPath("oc"); err != nil {
return fmt.Errorf("'oc' command not found in PATH. Install OpenShift CLI from: https://mirror.openshift.com/pub/openshift-v4/clients/ocp/")
}

// Validate dest-dir if specified
if o.DestDir != "" {
if !filepath.IsAbs(o.DestDir) {
// Convert to absolute path
absPath, err := filepath.Abs(o.DestDir)
if err != nil {
return fmt.Errorf("invalid dest-dir path: %w", err)
}
o.DestDir = absPath
}
}

return nil
}

// Run executes the must-gather command
func (o *MustGatherOptions) Run(c *cobra.Command, f client.Factory) error {
// Build command arguments
args := []string{"adm", "must-gather", "--image=" + o.effectiveImage}

// Add dest-dir if specified, otherwise use ./must-gather
if o.DestDir != "" {
args = append(args, "--dest-dir="+o.DestDir)
} else {
args = append(args, "--dest-dir=./must-gather")
}

// Add gather script arguments if any flags are set
if o.RequestTimeout > 0 || o.SkipTLS {
args = append(args, "--")
args = append(args, "/usr/bin/gather")

if o.RequestTimeout > 0 {
// Format duration for gather script (e.g., "1m", "30s")
args = append(args, "--request-timeout", o.RequestTimeout.String())
}

if o.SkipTLS {
args = append(args, "--skip-tls")
}
}

// Execute with real-time output streaming
cmd := exec.Command("oc", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

if err := cmd.Run(); err != nil {
return o.formatError(err)
}

return nil
}

// formatError formats error messages for common failure scenarios
func (o *MustGatherOptions) formatError(err error) error {
// Check if oc not installed (shouldn't happen as we validate, but defensive)
if errors.Is(err, exec.ErrNotFound) {
return fmt.Errorf("'oc' command not found. Install OpenShift CLI from: https://mirror.openshift.com/pub/openshift-v4/clients/ocp/")
}

// Check exit code for permission/auth issues
if exitErr, ok := err.(*exec.ExitError); ok {
return fmt.Errorf("must-gather failed with exit code %d. Check that you're logged in and have appropriate permissions", exitErr.ExitCode())
}

return fmt.Errorf("must-gather failed: %w", err)
}

// NewMustGatherCommand creates the must-gather command
func NewMustGatherCommand(f client.Factory) *cobra.Command {
o := &MustGatherOptions{}

c := &cobra.Command{
Use: "must-gather",
Short: "Collect diagnostic information for OADP",
Long: `Collect diagnostic information for OADP installations.

This command runs the OADP must-gather tool to collect logs and cluster state
information needed for troubleshooting and support cases. The diagnostic bundle
will be saved to the specified directory (or current directory by default).

Examples:
# Collect diagnostics to current directory
oc oadp must-gather

# Collect diagnostics to a specific directory
oc oadp must-gather --dest-dir=/tmp/oadp-diagnostics

# Collect diagnostics with custom timeout
oc oadp must-gather --request-timeout=30s

# Collect diagnostics and skip TLS verification
oc oadp must-gather --skip-tls

# Combine multiple options
oc oadp must-gather --dest-dir=/tmp/output --request-timeout=1m --skip-tls`,
Args: cobra.ExactArgs(0),
RunE: func(c *cobra.Command, args []string) error {
if err := o.Complete(args, f); err != nil {
return err
}
if err := o.Validate(c, args, f); err != nil {
return err
}
return o.Run(c, f)
},
}

o.BindFlags(c.Flags())

return c
}
115 changes: 115 additions & 0 deletions cmd/must-gather/must_gather_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
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 mustgather_test

import (
"strings"
"testing"

"github.com/migtools/oadp-cli/internal/testutil"
)

// TestMustGatherHelp verifies that the help command displays expected content
func TestMustGatherHelp(t *testing.T) {
binaryPath := testutil.BuildCLIBinary(t)

expectContains := []string{
"Collect diagnostic information",
"--dest-dir",
"--request-timeout",
"--skip-tls",
"Examples:",
}

testutil.TestHelpCommand(t, binaryPath, []string{"must-gather", "--help"}, expectContains)
}

// TestMustGatherHelpFlags verifies that both --help and -h work
func TestMustGatherHelpFlags(t *testing.T) {
binaryPath := testutil.BuildCLIBinary(t)

// Test both --help and -h work
for _, flag := range []string{"--help", "-h"} {
t.Run(flag, func(t *testing.T) {
output, err := testutil.RunCommand(t, binaryPath, "must-gather", flag)
if err != nil {
t.Errorf("Help command with %s failed: %v", flag, err)
}
if !strings.Contains(output, "Usage:") {
t.Errorf("Help output missing Usage section")
}
if !strings.Contains(output, "Collect diagnostic information") {
t.Errorf("Help output missing description")
}
})
}
}

// TestMustGatherHelpContent verifies specific help text content
func TestMustGatherHelpContent(t *testing.T) {
binaryPath := testutil.BuildCLIBinary(t)

output, err := testutil.RunCommand(t, binaryPath, "must-gather", "--help")
if err != nil {
t.Fatalf("Help command failed: %v", err)
}

// Verify all flags are documented
requiredContent := []string{
"--dest-dir",
"--request-timeout",
"--skip-tls",
"Directory where must-gather output will be stored",
"Timeout for the gather script",
"Skip TLS verification",
}

for _, content := range requiredContent {
if !strings.Contains(output, content) {
t.Errorf("Help output missing expected content: %q", content)
}
}

// Verify --image flag is hidden (should not appear in help)
if strings.Contains(output, "--image") {
t.Errorf("Hidden flag --image should not appear in help output")
}
}

// TestMustGatherExamples verifies that the help text includes examples
func TestMustGatherExamples(t *testing.T) {
binaryPath := testutil.BuildCLIBinary(t)

output, err := testutil.RunCommand(t, binaryPath, "must-gather", "--help")
if err != nil {
t.Fatalf("Help command failed: %v", err)
}

// Verify examples are present
examples := []string{
"oc oadp must-gather",
"--dest-dir=/tmp/oadp-diagnostics",
"--request-timeout=30s",
"--skip-tls",
}

for _, example := range examples {
if !strings.Contains(output, example) {
t.Errorf("Help output missing example: %q", example)
}
}
}
6 changes: 5 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"time"

"github.com/fatih/color"
mustgather "github.com/migtools/oadp-cli/cmd/must-gather"
"github.com/migtools/oadp-cli/cmd/nabsl-request"
nonadmin "github.com/migtools/oadp-cli/cmd/non-admin"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -418,11 +419,14 @@ func NewVeleroRootCommand(baseName string) *cobra.Command {
// Custom subcommands - use NonAdmin factory
c.AddCommand(nonadmin.NewNonAdminCommand(f))

// Must-gather command - diagnostic tool
c.AddCommand(mustgather.NewMustGatherCommand(f))

// Apply velero->oadp replacement to all commands recursively
// Skip nonadmin commands since we have full control over their output
for _, cmd := range c.Commands() {
// Don't wrap nonadmin commands - we control them and they already use correct terminology
if cmd.Use == "nonadmin" || cmd.Use == "nabsl-request" {
if cmd.Use == "nonadmin" || cmd.Use == "nabsl-request" || cmd.Use == "must-gather" {
continue
}
replaceVeleroWithOADP(cmd)
Expand Down
Loading