Files
wild-cloud/wild-cli/cmd/wild/app/doctor.go

247 lines
8.0 KiB
Go

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
}