Skip to content
Merged
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
103 changes: 84 additions & 19 deletions internal/cli/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"github.com/ork-cli/ork/internal/config"
"github.com/ork-cli/ork/internal/docker"
"github.com/ork-cli/ork/internal/service"
"github.com/ork-cli/ork/internal/ui"
"github.com/ork-cli/ork/pkg/utils"
"github.com/spf13/cobra"
)

Expand All @@ -29,7 +31,7 @@ var upCmd = &cobra.Command{
Args: cobra.MinimumNArgs(1), // Require at least one service name
Run: func(cmd *cobra.Command, args []string) {
if err := runUp(args); err != nil {
fmt.Printf("❌ Error: %v\n", err)
handleUpError(err)
return
}
},
Expand Down Expand Up @@ -68,30 +70,44 @@ func runUp(serviceNames []string) error {
}
defer func() {
if closeErr := dockerClient.Close(); closeErr != nil {
fmt.Printf("❌ Error closing Docker client: %v\n", closeErr)
ui.Warning(fmt.Sprintf("Failed to close Docker client: %v", closeErr))
}
}()

// Resolve dependencies and get services in the correct start order
orderedServices, err := service.ResolveDependencies(cfg.Services, serviceNames)
if err != nil {
return fmt.Errorf("failed to resolve dependencies: %w", err)
return utils.ServiceError(
"up.dependencies",
"Failed to resolve service dependencies",
"Check your service dependencies in ork.yml",
err,
)
}

// Create a project network for service communication
ctx := context.Background()
spinner := ui.ShowSpinner("Creating project network...")
networkID, err := dockerClient.CreateNetwork(ctx, cfg.Project)
if err != nil {
return fmt.Errorf("failed to create project network: %w", err)
spinner.Error("Failed to create network")
return utils.NetworkError(
"up.network",
"Failed to create project network",
"Check if Docker is running and you have permissions",
err,
)
}
fmt.Printf("🌐 Created network: ork-%s-network\n", cfg.Project)
spinner.Success(fmt.Sprintf("Created network: ork-%s-network", cfg.Project))

// Show startup message
fmt.Printf("✅ Loaded project: %s (version %s)\n", cfg.Project, cfg.Version)
fmt.Printf("🚀 Starting services: %v\n", serviceNames)
// Show startup summary
ui.EmptyLine()
ui.Info(fmt.Sprintf("Project: %s (v%s)", ui.Bold(cfg.Project), cfg.Version))
ui.Info(fmt.Sprintf("Starting: %s", ui.Highlight(fmt.Sprintf("%v", serviceNames))))
if len(orderedServices) > len(serviceNames) {
fmt.Printf("📦 Including dependencies: %v\n", orderedServices)
ui.Info(fmt.Sprintf("Dependencies: %s", ui.Dim(fmt.Sprintf("%v", orderedServices))))
}
ui.EmptyLine()

// Create an orchestrator for parallel service management
orchestrator := service.NewOrchestrator(cfg.Project, dockerClient, networkID)
Expand All @@ -106,7 +122,8 @@ func runUp(serviceNames []string) error {
return err
}

fmt.Printf("✅ All services started successfully!\n")
ui.EmptyLine()
ui.SuccessBox(fmt.Sprintf("All services started successfully! %s", ui.SymbolRocket))
return nil
}

Expand All @@ -118,11 +135,21 @@ func runUp(serviceNames []string) error {
func loadAndValidateConfig() (*config.Config, error) {
cfg, err := config.Load()
if err != nil {
return nil, fmt.Errorf("failed to load config: %w", err)
return nil, utils.ConfigError(
"up.load",
"Failed to load configuration",
"Make sure ork.yml exists in the current directory",
err,
)
}

if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("invalid configuration: %w", err)
return nil, utils.ConfigError(
"up.validate",
"Invalid configuration",
"Check your ork.yml for errors",
err,
)
}

return cfg, nil
Expand All @@ -136,18 +163,24 @@ func loadAndValidateConfig() (*config.Config, error) {
func validateServiceNames(serviceNames []string, cfg *config.Config) error {
for _, serviceName := range serviceNames {
if _, exists := cfg.Services[serviceName]; !exists {
return fmt.Errorf("service '%s' not found in ork.yml\n💡 Available services: %s",
serviceName, getAvailableServicesList(cfg))
availableServices := getAvailableServicesList(cfg)
suggestions := utils.FindSuggestions(serviceName, availableServices, 3)

err := utils.ErrServiceNotFound(serviceName, suggestions)
err.Details = []string{
fmt.Sprintf("Available services: %s", ui.Dim(fmt.Sprintf("%v", availableServices))),
}
return err
}
}
return nil
}

// getAvailableServicesList returns a formatted string of available services
func getAvailableServicesList(cfg *config.Config) string {
services := ""
// getAvailableServicesList returns a slice of available service names
func getAvailableServicesList(cfg *config.Config) []string {
services := make([]string, 0, len(cfg.Services))
for name := range cfg.Services {
services += name + " "
services = append(services, name)
}
return services
}
Expand All @@ -160,7 +193,39 @@ func getAvailableServicesList(cfg *config.Config) string {
func createDockerClient() (*docker.Client, error) {
client, err := docker.NewClient()
if err != nil {
return nil, fmt.Errorf("failed to create Docker client: %w", err)
return nil, utils.DockerError(
"up.docker",
"Failed to connect to Docker",
"Make sure Docker is running. Try 'docker ps' or run 'ork doctor'",
err,
)
}
return client, nil
}

// handleUpError formats and displays errors with hints
func handleUpError(err error) {
if orkErr, ok := err.(*utils.OrkError); ok {
// Display structured error with hints
ui.Error(orkErr.Message)
if orkErr.Hint != "" {
ui.Hint(orkErr.Hint)
}
if len(orkErr.Details) > 0 {
ui.EmptyLine()
for _, detail := range orkErr.Details {
ui.List(detail)
}
}
if len(orkErr.Suggestions) > 0 {
ui.EmptyLine()
ui.Info("Did you mean:")
for _, suggestion := range orkErr.Suggestions {
ui.ListItem(ui.SymbolArrow, ui.Highlight(suggestion))
}
}
} else {
// Fallback for non-Ork errors
ui.Error(fmt.Sprintf("Error: %v", err))
}
}
38 changes: 23 additions & 15 deletions internal/service/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/ork-cli/ork/internal/config"
"github.com/ork-cli/ork/internal/docker"
"github.com/ork-cli/ork/internal/ui"
)

// ============================================================================
Expand Down Expand Up @@ -67,20 +68,20 @@ func (o *Orchestrator) StartServicesInOrder(ctx context.Context, orderedServiceN

// Start services level by level
for levelNum, levelServices := range levels {
fmt.Printf("📦 Starting level %d: %v\n", levelNum+1, levelServices)
ui.Subheader(fmt.Sprintf("Level %d: %s", levelNum+1, ui.Dim(fmt.Sprintf("%v", levelServices))))

// Start all services in this level in parallel
if err := o.startServicesInParallel(ctx, levelServices, &startedServices); err != nil {
// Rollback on failure
fmt.Printf("❌ Failed to start services: %v\n", err)
ui.Error(fmt.Sprintf("Failed to start services: %v", err))
o.rollbackStartedServices(ctx, startedServices)
return err
}

// Wait for all services in this level to become healthy
if err := o.waitForHealthy(ctx, levelServices); err != nil {
// Rollback on health check failure
fmt.Printf("❌ Health check failed: %v\n", err)
ui.Error(fmt.Sprintf("Health check failed: %v", err))
o.rollbackStartedServices(ctx, startedServices)
return err
}
Expand Down Expand Up @@ -179,7 +180,7 @@ func (o *Orchestrator) calculateServiceLevel(serviceName string, graph map[strin
func (o *Orchestrator) startServicesInParallel(ctx context.Context, serviceNames []string, startedServices *[]*Service) error {
// Use a wait group to track parallel starts
var wg sync.WaitGroup
var mu sync.Mutex // Protects concurrent access to startedServices slice
var mu sync.Mutex // Protects concurrent access to the startedServices slice
errChan := make(chan error, len(serviceNames))

// Start each service in a separate goroutine
Expand All @@ -195,14 +196,19 @@ func (o *Orchestrator) startServicesInParallel(ctx context.Context, serviceNames
return
}

// Start the service
fmt.Printf("🐳 Starting %s...\n", serviceName)
// Start the service with a spinner
spinner := ui.ShowSpinner(fmt.Sprintf("Starting %s", ui.Bold(serviceName)))
if err := svc.Start(ctx, o.dockerClient, o.networkID); err != nil {
spinner.Error(fmt.Sprintf("Failed to start %s", serviceName))
errChan <- fmt.Errorf("failed to start %s: %w", serviceName, err)
return
}

fmt.Printf("✅ Started %s (container: %s)\n", serviceName, svc.GetContainerID()[:12])
containerID := svc.GetContainerID()
if len(containerID) > 12 {
containerID = containerID[:12]
}
spinner.Success(fmt.Sprintf("Started %s %s", ui.Bold(serviceName), ui.Dim(containerID)))

// Track successfully started service (protected by mutex)
mu.Lock()
Expand Down Expand Up @@ -251,7 +257,7 @@ func (o *Orchestrator) waitForHealthy(ctx context.Context, serviceNames []string
return nil
}

fmt.Printf("🏥 Waiting for services to become healthy...\n")
ui.Info(fmt.Sprintf("%s Waiting for health checks...", ui.SymbolDoctor))

// Wait for each service with a health check
var wg sync.WaitGroup
Expand All @@ -278,7 +284,7 @@ func (o *Orchestrator) waitForHealthy(ctx context.Context, serviceNames []string
return
}

fmt.Printf("✅ %s is healthy\n", service.Name)
ui.Success(fmt.Sprintf("%s is healthy", service.Name))
}(svc)
}

Expand Down Expand Up @@ -344,17 +350,18 @@ func (o *Orchestrator) rollbackStartedServices(ctx context.Context, startedServi
return
}

fmt.Printf("🔄 Rolling back %d started service(s)...\n", len(startedServices))
ui.EmptyLine()
ui.Warning(fmt.Sprintf("Rolling back %d started service(s)...", len(startedServices)))

// Stop services in reverse order
for i := len(startedServices) - 1; i >= 0; i-- {
svc := startedServices[i]
fmt.Printf("🛑 Rolling back %s...\n", svc.Name)
spinner := ui.ShowSpinner(fmt.Sprintf("Rolling back %s", svc.Name))

if err := svc.Stop(ctx, o.dockerClient); err != nil {
fmt.Printf("⚠️ Warning: failed to rollback %s: %v\n", svc.Name, err)
spinner.Warning(fmt.Sprintf("Failed to rollback %s: %v", svc.Name, err))
} else {
fmt.Printf("✅ Rolled back %s\n", svc.Name)
spinner.Success(fmt.Sprintf("Rolled back %s", svc.Name))
}
}
}
Expand Down Expand Up @@ -386,12 +393,13 @@ func (o *Orchestrator) StopAll(ctx context.Context) error {
go func(service *Service) {
defer wg.Done()

fmt.Printf("🛑 Stopping %s...\n", service.Name)
spinner := ui.ShowSpinner(fmt.Sprintf("Stopping %s", service.Name))
if err := service.Stop(ctx, o.dockerClient); err != nil {
spinner.Error(fmt.Sprintf("Failed to stop %s", service.Name))
errChan <- fmt.Errorf("failed to stop %s: %w", service.Name, err)
return
}
fmt.Printf("✅ Stopped %s\n", service.Name)
spinner.Success(fmt.Sprintf("Stopped %s", service.Name))
}(svc)
}

Expand Down
Loading