diff --git a/cmd/must-gather/must_gather.go b/cmd/must-gather/must_gather.go new file mode 100644 index 0000000..e2b80b4 --- /dev/null +++ b/cmd/must-gather/must_gather.go @@ -0,0 +1,187 @@ +/* +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 ( + "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() + } 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 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 a Cobra command that runs the OADP must-gather tool to collect diagnostic information. +// The returned command is configured with flags from MustGatherOptions and a RunE handler that calls Complete, Validate, and Run. +// The provided client.Factory supplies clients used by the command at runtime. +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 +} \ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go index ac757f1..da27258 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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" @@ -348,7 +349,9 @@ func wrapPreRunE(existing func(*cobra.Command, []string) error, additional func( } } -// NewVeleroRootCommand returns a root command with all Velero CLI subcommands attached. +// NewVeleroRootCommand returns a configured root cobra.Command for the OADP CLI with all Velero subcommands and related wiring. +// +// The returned command mounts Velero subcommands (backup, schedule, restore, etc.), the admin NABSL request command, non-admin custom commands, and the must-gather diagnostic command. It binds namespace-related flags to the client factory, wraps the factory to apply a global request timeout controlled by --request-timeout, applies Velero→OADP text replacements to command examples and output (skipping nonadmin, nabsl-request, and must-gather), renames --timeout flags to --request-timeout recursively, customizes the usage template to show the "oc" prefix, and initializes klog flags. func NewVeleroRootCommand(baseName string) *cobra.Command { config, err := clientcmd.LoadConfig() @@ -418,11 +421,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) @@ -442,4 +448,4 @@ func NewVeleroRootCommand(baseName string) *cobra.Command { klog.InitFlags(flag.CommandLine) c.PersistentFlags().AddGoFlagSet(flag.CommandLine) return c -} +} \ No newline at end of file