First commit of golang CLI.
This commit is contained in:
246
wild-cli/cmd/wild/app/doctor.go
Normal file
246
wild-cli/cmd/wild/app/doctor.go
Normal file
@@ -0,0 +1,246 @@
|
||||
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
|
||||
}
|
Reference in New Issue
Block a user