Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 14 additions & 60 deletions cmd/down.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,109 +6,63 @@ 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) {
startTime := time.Now()
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
cfg, err := config.Load(configPath, version)
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
}
75 changes: 65 additions & 10 deletions cmd/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
Expand All @@ -21,7 +22,8 @@ import (
)

type UpOptions struct {
Demo bool
Demo bool
SkipPreflight bool
}

var upOptions = &UpOptions{}
Expand All @@ -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
}

Expand Down Expand Up @@ -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
Expand Down