diff --git a/cmd/down.go b/cmd/down.go index 487a2d3..41bacef 100644 --- a/cmd/down.go +++ b/cmd/down.go @@ -6,30 +6,25 @@ import ( "time" "github.com/glassflow/glassflow-cli/internal/config" - "github.com/glassflow/glassflow-cli/internal/helm" - "github.com/glassflow/glassflow-cli/internal/install" "github.com/glassflow/glassflow-cli/internal/k8s" "github.com/glassflow/glassflow-cli/internal/tracking" "github.com/spf13/cobra" ) -type DownOptions struct { - Force bool -} - -var downOptions = &DownOptions{} - var downCmd = &cobra.Command{ Use: "down", Short: "Stop local development environment", - Long: `Stop the local GlassFlow development environment and clean up resources.`, + Long: `Stop the local GlassFlow development environment: kill port forwards and delete the Kind cluster.`, RunE: runDown, } +var forceDown bool + func init() { rootCmd.AddCommand(downCmd) - - downCmd.Flags().BoolVar(&downOptions.Force, "force", false, "Force cleanup even if resources are in use") + // Kept for backward compatibility — glassflow down always deletes the cluster directly + downCmd.Flags().BoolVar(&forceDown, "force", false, "Kept for backward compatibility (no-op, cluster is always deleted directly)") + _ = downCmd.Flags().MarkHidden("force") } func runDown(cmd *cobra.Command, args []string) (err error) { @@ -37,16 +32,12 @@ func runDown(cmd *cobra.Command, args []string) (err error) { defer func() { elapsed := time.Since(startTime) if err != nil { - tracking.TrackDownFailed(version, downOptions.Force, err, elapsed) + tracking.TrackDownFailed(version, false, err, elapsed) } else { - tracking.TrackDownCompleted(version, downOptions.Force, elapsed) + tracking.TrackDownCompleted(version, false, elapsed) } }() - if verbose { - fmt.Printf("Stopping GlassFlow environment in namespace=glassflow, force=%v\n", downOptions.Force) - } - fmt.Println("🛑 Stopping GlassFlow local development environment...") // Load configuration @@ -54,61 +45,24 @@ func runDown(cmd *cobra.Command, args []string) (err error) { if err != nil { return fmt.Errorf("failed to load config: %w", err) } - kubeContext := resolveKubeContext(cfg) - // Clean up only the port-forwards started by our CLI + // Clean up port-forwards started by our CLI fmt.Println("🔗 Cleaning up port forwarding...") k8s.CleanupPortForwarding(verbose) - // Override namespace if specified - if cfg.Namespace != "glassflow" { - cfg.Namespace = "glassflow" - } - - // Initialize managers + // Delete the Kind cluster (removes all Helm releases, pods, PVCs with it) k8sManager := k8s.NewManager(&k8s.Config{ ClusterName: cfg.KindClusterName, - Namespace: cfg.Namespace, + Namespace: "glassflow", Kubeconfig: cfg.Kubeconfig, - Context: kubeContext, + Context: resolveKubeContext(cfg), }) - client, err := k8sManager.GetKubernetesClient() - if err != nil { - return fmt.Errorf("failed to get Kubernetes client: %w", err) - } - - helmManager := helm.NewManager(client, &helm.Config{ - Namespace: cfg.Namespace, - Kubeconfig: cfg.Kubeconfig, - Context: kubeContext, - Repositories: []helm.Repository{}, - Verbose: verbose, - }) - - installManager := install.NewManager(helmManager, k8sManager, &install.Config{ - Namespace: cfg.Namespace, - Demo: true, // Always try to uninstall demo services if they exist - Charts: &cfg.Charts, - KubeContext: kubeContext, - }) - - // Stop environment ctx := context.Background() - if downOptions.Force { - // Force mode: directly delete the Kind cluster without waiting for Helm uninstallation - fmt.Println("⚠️ Force mode: directly deleting Kind cluster...") - if err := k8sManager.DeleteCluster(ctx); err != nil { - return fmt.Errorf("failed to delete Kind cluster: %w", err) - } - } else { - // Normal mode: try to uninstall Helm releases first, then delete cluster - if err := installManager.StopEnvironment(ctx); err != nil { - return fmt.Errorf("failed to stop environment: %w", err) - } + if err := k8sManager.DeleteCluster(ctx); err != nil { + return fmt.Errorf("failed to delete Kind cluster: %w", err) } fmt.Println("✅ GlassFlow environment stopped successfully!") - return nil } diff --git a/cmd/up.go b/cmd/up.go index 40aa0fe..7429fe6 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "encoding/json" "fmt" "io" "net/http" @@ -21,7 +22,8 @@ import ( ) type UpOptions struct { - Demo bool + Demo bool + SkipPreflight bool } var upOptions = &UpOptions{} @@ -37,15 +39,66 @@ func init() { rootCmd.AddCommand(upCmd) upCmd.Flags().BoolVar(&upOptions.Demo, "demo", false, "Also install Kafka + ClickHouse and set up a demo pipeline") + upCmd.Flags().BoolVar(&upOptions.SkipPreflight, "skip-preflight", false, "Skip preflight checks (Docker resources, binary checks)") } -// checkDockerRuntime ensures a Docker-compatible runtime is available by invoking `docker info`. -// We intentionally do not detect specific providers; users can choose any Docker-compatible runtime. -func checkDockerRuntime() error { - cmd := exec.Command("docker", "info") - if err := cmd.Run(); err != nil { - return fmt.Errorf("no Docker-compatible runtime detected. Please install and start a Docker-compatible runtime (e.g., Docker Desktop, OrbStack, Colima, or Podman) and ensure 'docker info' succeeds: %w", err) +// runPreflightChecks validates all prerequisites before starting the environment. +// Collects all errors and reports them together so the user can fix everything at once. +func runPreflightChecks() error { + fmt.Println("🔍 Running preflight checks...") + var errors []string + var warnings []string + + // Check Docker + dockerCmd := exec.Command("docker", "info", "--format", "json") + dockerOut, err := dockerCmd.Output() + if err != nil { + errors = append(errors, "Docker is not running. Please install and start a Docker-compatible runtime (Docker Desktop, OrbStack, Colima, or Podman).") + } else { + // Parse Docker info for resource checks + var info struct { + MemTotal int64 `json:"MemTotal"` + NCPU int `json:"NCPU"` + } + if json.Unmarshal(dockerOut, &info) == nil { + memGB := float64(info.MemTotal) / (1024 * 1024 * 1024) + if memGB < 2 { + errors = append(errors, fmt.Sprintf("Docker has %.1f GB RAM. GlassFlow requires at least 4 GB. Update in Docker Desktop > Settings > Resources.", memGB)) + } else if memGB < 4 { + warnings = append(warnings, fmt.Sprintf("Docker has %.1f GB RAM. GlassFlow recommends at least 4 GB for reliable operation. Update in Docker Desktop > Settings > Resources.", memGB)) + } + if info.NCPU < 2 { + warnings = append(warnings, fmt.Sprintf("Docker has %d CPU(s). GlassFlow recommends at least 2 CPUs.", info.NCPU)) + } + } + } + + // Check Helm + if _, err := exec.LookPath("helm"); err != nil { + errors = append(errors, "helm is not installed. Install with: brew install helm (or see https://helm.sh/docs/intro/install/)") + } + + // Check kubectl + if _, err := exec.LookPath("kubectl"); err != nil { + errors = append(errors, "kubectl is not installed. Install with: brew install kubectl (or see https://kubernetes.io/docs/tasks/tools/)") } + + // Report warnings + for _, w := range warnings { + fmt.Printf(" ⚠️ %s\n", w) + } + + // Report errors + if len(errors) > 0 { + fmt.Println() + for _, e := range errors { + fmt.Printf(" ❌ %s\n", e) + } + fmt.Println() + return fmt.Errorf("preflight checks failed (%d error(s)). Fix the issues above and try again", len(errors)) + } + + fmt.Println(" ✅ All preflight checks passed") return nil } @@ -192,9 +245,11 @@ func runUp(cmd *cobra.Command, args []string) (err error) { fmt.Println("🚀 Starting GlassFlow local development environment...") tracking.TrackUpStarted(version, upOptions.Demo) - // Preflight: verify a Docker-compatible runtime is available - if err = checkDockerRuntime(); err != nil { - return err + // Preflight checks + if !upOptions.SkipPreflight { + if err = runPreflightChecks(); err != nil { + return err + } } // Load configuration