package app import ( "context" "fmt" "os" "path/filepath" "strings" "time" "github.com/spf13/cobra" "github.com/wild-cloud/wild-cli/internal/environment" "github.com/wild-cloud/wild-cli/internal/external" "github.com/wild-cloud/wild-cli/internal/output" ) var ( doctorKeep bool doctorFollow bool doctorTimeout int ) func newDoctorCommand() *cobra.Command { cmd := &cobra.Command{ Use: "doctor [name]", Short: "Check application health", Long: `Run diagnostic tests for an application. This command runs diagnostic tests for applications that have a doctor/ directory with a kustomization.yaml file. The tests run as Kubernetes jobs and provide detailed health and connectivity information. Arguments: name Name of the app to diagnose (must have apps/name/doctor/ directory) Options: --keep Keep diagnostic resources after completion (don't auto-cleanup) --follow Follow logs in real-time instead of waiting for completion --timeout SECONDS Timeout for job completion (default: 120 seconds) Examples: wild app doctor postgres wild app doctor postgres --follow wild app doctor postgres --keep --timeout 300`, Args: cobra.MaximumNArgs(1), RunE: runAppDoctor, } cmd.Flags().BoolVar(&doctorKeep, "keep", false, "keep diagnostic resources after completion (don't auto-cleanup)") cmd.Flags().BoolVar(&doctorFollow, "follow", false, "follow logs in real-time instead of waiting for completion") cmd.Flags().IntVar(&doctorTimeout, "timeout", 120, "timeout for job completion in seconds") return cmd } func runAppDoctor(cmd *cobra.Command, args []string) error { // Initialize environment env := environment.New() if err := env.RequiresProject(); err != nil { return err } // Initialize external tools toolManager := external.NewManager() if err := toolManager.CheckTools(cmd.Context(), []string{"kubectl"}); err != nil { return fmt.Errorf("required tools not available: %w", err) } kubectl := toolManager.Kubectl() // If no app name provided, list available doctors if len(args) == 0 { return listAvailableDoctors(env.AppsDir()) } appName := args[0] // Check if doctor directory exists doctorDir := filepath.Join(env.AppsDir(), appName, "doctor") if _, err := os.Stat(doctorDir); os.IsNotExist(err) { output.Error(fmt.Sprintf("Doctor directory not found: %s", doctorDir)) output.Info("") return listAvailableDoctors(env.AppsDir()) } // Check if kustomization.yaml exists kustomizationFile := filepath.Join(doctorDir, "kustomization.yaml") if _, err := os.Stat(kustomizationFile); os.IsNotExist(err) { return fmt.Errorf("kustomization.yaml not found in %s", doctorDir) } output.Header(fmt.Sprintf("Running diagnostics for: %s", appName)) output.Info(fmt.Sprintf("Doctor directory: %s", doctorDir)) output.Info("") // Extract namespace and job name before applying namespace, jobName, err := extractJobInfo(cmd.Context(), kubectl, doctorDir) if err != nil { return fmt.Errorf("failed to extract job information: %w", err) } // Set up cleanup function cleanup := func() { if !doctorKeep { output.Info("Cleaning up diagnostic resources...") deleteArgs := []string{"delete", "-k", doctorDir} if _, err := kubectl.Execute(cmd.Context(), deleteArgs...); err != nil { output.Info(" (No resources to clean up)") } } else { output.Info("Keeping diagnostic resources (--keep flag specified)") output.Info(fmt.Sprintf(" To manually cleanup later: kubectl delete -k %s", doctorDir)) } } defer cleanup() // Delete existing job if it exists (to avoid conflicts) deleteJobArgs := []string{"delete", "job", jobName, "-n", namespace} _, _ = kubectl.Execute(cmd.Context(), deleteJobArgs...) // Apply the doctor kustomization output.Info("Deploying diagnostic resources...") applyArgs := []string{"apply", "-k", doctorDir} if _, err := kubectl.Execute(cmd.Context(), applyArgs...); err != nil { return fmt.Errorf("failed to apply diagnostic resources: %w", err) } output.Info(fmt.Sprintf("Monitoring job: %s (namespace: %s)", jobName, namespace)) if doctorFollow { output.Info("Following logs in real-time (Ctrl+C to stop)...") output.Info("────────────────────────────────────────────────────────────────") // Wait a moment for the pod to be created time.Sleep(5 * time.Second) logsArgs := []string{"logs", "-f", "job/" + jobName, "-n", namespace} _, _ = kubectl.Execute(cmd.Context(), logsArgs...) } else { // Wait for job completion output.Info(fmt.Sprintf("Waiting for diagnostics to complete (timeout: %ds)...", doctorTimeout)) waitArgs := []string{"wait", "--for=condition=complete", "job/" + jobName, "-n", namespace, fmt.Sprintf("--timeout=%ds", doctorTimeout)} if _, err := kubectl.Execute(cmd.Context(), waitArgs...); err != nil { output.Warning(fmt.Sprintf("Job did not complete within %d seconds", doctorTimeout)) output.Info("Showing current logs:") output.Info("────────────────────────────────────────────────────────────────") logsArgs := []string{"logs", "job/" + jobName, "-n", namespace} if logsOutput, err := kubectl.Execute(cmd.Context(), logsArgs...); err == nil { output.Printf("%s\n", string(logsOutput)) } else { output.Info("Could not retrieve logs") } return fmt.Errorf("diagnostic job did not complete within timeout") } // Show the results output.Success("Diagnostics completed successfully!") output.Info("Results:") output.Info("────────────────────────────────────────────────────────────────") logsArgs := []string{"logs", "job/" + jobName, "-n", namespace} if logsOutput, err := kubectl.Execute(cmd.Context(), logsArgs...); err == nil { output.Printf("%s\n", string(logsOutput)) } } output.Info("────────────────────────────────────────────────────────────────") output.Success(fmt.Sprintf("Diagnostics for %s completed!", appName)) return nil } func listAvailableDoctors(appsDir string) error { output.Info("Available doctors:") entries, err := os.ReadDir(appsDir) if err != nil { return fmt.Errorf("failed to read apps directory: %w", err) } found := false for _, entry := range entries { if entry.IsDir() { doctorDir := filepath.Join(appsDir, entry.Name(), "doctor") if _, err := os.Stat(doctorDir); err == nil { output.Info(fmt.Sprintf(" - %s", entry.Name())) found = true } } } if !found { output.Info(" (none found)") } return fmt.Errorf("app name required") } func extractJobInfo(ctx context.Context, kubectl *external.KubectlTool, doctorDir string) (string, string, error) { // Run kubectl kustomize to get the rendered YAML kustomizeArgs := []string{"kustomize", doctorDir} output, err := kubectl.Execute(ctx, kustomizeArgs...) if err != nil { return "", "", fmt.Errorf("failed to run kubectl kustomize: %w", err) } yamlStr := string(output) lines := strings.Split(yamlStr, "\n") var namespace, jobName string // Look for namespace for _, line := range lines { if strings.Contains(line, "namespace:") { parts := strings.Fields(line) if len(parts) >= 2 { namespace = parts[1] break } } } if namespace == "" { namespace = "default" } // Look for job name inJob := false for _, line := range lines { if strings.Contains(line, "kind: Job") { inJob = true continue } if inJob && strings.Contains(line, "name:") { parts := strings.Fields(line) if len(parts) >= 2 { jobName = parts[1] break } } } if jobName == "" { return "", "", fmt.Errorf("could not find job name in kustomization") } return namespace, jobName, nil }