247 lines
8.0 KiB
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
|
|
}
|