Skip to content

Commit be1ce48

Browse files
authored
feat: Enhance up and service orchestration with consistent UI and error handling (#5)
- Replace `fmt` outputs with structured UI helpers (`ui.Info`, `ui.Warning`, `ui.Success`, etc.) for better feedback. - Standardize error handling using new `utils` error types (`ConfigError`, `ServiceError`, `DockerError`, etc.) with hints and suggestions. - Introduce spinners for status updates during network creation, service startup, and rollback operations. - Refine service orchestration logic to use enhanced UI messaging and improve user experience. Signed-off-by: Harry Dhillon <harry@harryy.dev>
1 parent ad7a68d commit be1ce48

2 files changed

Lines changed: 107 additions & 34 deletions

File tree

internal/cli/up.go

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"github.com/ork-cli/ork/internal/config"
88
"github.com/ork-cli/ork/internal/docker"
99
"github.com/ork-cli/ork/internal/service"
10+
"github.com/ork-cli/ork/internal/ui"
11+
"github.com/ork-cli/ork/pkg/utils"
1012
"github.com/spf13/cobra"
1113
)
1214

@@ -29,7 +31,7 @@ var upCmd = &cobra.Command{
2931
Args: cobra.MinimumNArgs(1), // Require at least one service name
3032
Run: func(cmd *cobra.Command, args []string) {
3133
if err := runUp(args); err != nil {
32-
fmt.Printf("❌ Error: %v\n", err)
34+
handleUpError(err)
3335
return
3436
}
3537
},
@@ -68,30 +70,44 @@ func runUp(serviceNames []string) error {
6870
}
6971
defer func() {
7072
if closeErr := dockerClient.Close(); closeErr != nil {
71-
fmt.Printf("❌ Error closing Docker client: %v\n", closeErr)
73+
ui.Warning(fmt.Sprintf("Failed to close Docker client: %v", closeErr))
7274
}
7375
}()
7476

7577
// Resolve dependencies and get services in the correct start order
7678
orderedServices, err := service.ResolveDependencies(cfg.Services, serviceNames)
7779
if err != nil {
78-
return fmt.Errorf("failed to resolve dependencies: %w", err)
80+
return utils.ServiceError(
81+
"up.dependencies",
82+
"Failed to resolve service dependencies",
83+
"Check your service dependencies in ork.yml",
84+
err,
85+
)
7986
}
8087

8188
// Create a project network for service communication
8289
ctx := context.Background()
90+
spinner := ui.ShowSpinner("Creating project network...")
8391
networkID, err := dockerClient.CreateNetwork(ctx, cfg.Project)
8492
if err != nil {
85-
return fmt.Errorf("failed to create project network: %w", err)
93+
spinner.Error("Failed to create network")
94+
return utils.NetworkError(
95+
"up.network",
96+
"Failed to create project network",
97+
"Check if Docker is running and you have permissions",
98+
err,
99+
)
86100
}
87-
fmt.Printf("🌐 Created network: ork-%s-network\n", cfg.Project)
101+
spinner.Success(fmt.Sprintf("Created network: ork-%s-network", cfg.Project))
88102

89-
// Show startup message
90-
fmt.Printf("✅ Loaded project: %s (version %s)\n", cfg.Project, cfg.Version)
91-
fmt.Printf("🚀 Starting services: %v\n", serviceNames)
103+
// Show startup summary
104+
ui.EmptyLine()
105+
ui.Info(fmt.Sprintf("Project: %s (v%s)", ui.Bold(cfg.Project), cfg.Version))
106+
ui.Info(fmt.Sprintf("Starting: %s", ui.Highlight(fmt.Sprintf("%v", serviceNames))))
92107
if len(orderedServices) > len(serviceNames) {
93-
fmt.Printf("📦 Including dependencies: %v\n", orderedServices)
108+
ui.Info(fmt.Sprintf("Dependencies: %s", ui.Dim(fmt.Sprintf("%v", orderedServices))))
94109
}
110+
ui.EmptyLine()
95111

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

109-
fmt.Printf("✅ All services started successfully!\n")
125+
ui.EmptyLine()
126+
ui.SuccessBox(fmt.Sprintf("All services started successfully! %s", ui.SymbolRocket))
110127
return nil
111128
}
112129

@@ -118,11 +135,21 @@ func runUp(serviceNames []string) error {
118135
func loadAndValidateConfig() (*config.Config, error) {
119136
cfg, err := config.Load()
120137
if err != nil {
121-
return nil, fmt.Errorf("failed to load config: %w", err)
138+
return nil, utils.ConfigError(
139+
"up.load",
140+
"Failed to load configuration",
141+
"Make sure ork.yml exists in the current directory",
142+
err,
143+
)
122144
}
123145

124146
if err := cfg.Validate(); err != nil {
125-
return nil, fmt.Errorf("invalid configuration: %w", err)
147+
return nil, utils.ConfigError(
148+
"up.validate",
149+
"Invalid configuration",
150+
"Check your ork.yml for errors",
151+
err,
152+
)
126153
}
127154

128155
return cfg, nil
@@ -136,18 +163,24 @@ func loadAndValidateConfig() (*config.Config, error) {
136163
func validateServiceNames(serviceNames []string, cfg *config.Config) error {
137164
for _, serviceName := range serviceNames {
138165
if _, exists := cfg.Services[serviceName]; !exists {
139-
return fmt.Errorf("service '%s' not found in ork.yml\n💡 Available services: %s",
140-
serviceName, getAvailableServicesList(cfg))
166+
availableServices := getAvailableServicesList(cfg)
167+
suggestions := utils.FindSuggestions(serviceName, availableServices, 3)
168+
169+
err := utils.ErrServiceNotFound(serviceName, suggestions)
170+
err.Details = []string{
171+
fmt.Sprintf("Available services: %s", ui.Dim(fmt.Sprintf("%v", availableServices))),
172+
}
173+
return err
141174
}
142175
}
143176
return nil
144177
}
145178

146-
// getAvailableServicesList returns a formatted string of available services
147-
func getAvailableServicesList(cfg *config.Config) string {
148-
services := ""
179+
// getAvailableServicesList returns a slice of available service names
180+
func getAvailableServicesList(cfg *config.Config) []string {
181+
services := make([]string, 0, len(cfg.Services))
149182
for name := range cfg.Services {
150-
services += name + " "
183+
services = append(services, name)
151184
}
152185
return services
153186
}
@@ -160,7 +193,39 @@ func getAvailableServicesList(cfg *config.Config) string {
160193
func createDockerClient() (*docker.Client, error) {
161194
client, err := docker.NewClient()
162195
if err != nil {
163-
return nil, fmt.Errorf("failed to create Docker client: %w", err)
196+
return nil, utils.DockerError(
197+
"up.docker",
198+
"Failed to connect to Docker",
199+
"Make sure Docker is running. Try 'docker ps' or run 'ork doctor'",
200+
err,
201+
)
164202
}
165203
return client, nil
166204
}
205+
206+
// handleUpError formats and displays errors with hints
207+
func handleUpError(err error) {
208+
if orkErr, ok := err.(*utils.OrkError); ok {
209+
// Display structured error with hints
210+
ui.Error(orkErr.Message)
211+
if orkErr.Hint != "" {
212+
ui.Hint(orkErr.Hint)
213+
}
214+
if len(orkErr.Details) > 0 {
215+
ui.EmptyLine()
216+
for _, detail := range orkErr.Details {
217+
ui.List(detail)
218+
}
219+
}
220+
if len(orkErr.Suggestions) > 0 {
221+
ui.EmptyLine()
222+
ui.Info("Did you mean:")
223+
for _, suggestion := range orkErr.Suggestions {
224+
ui.ListItem(ui.SymbolArrow, ui.Highlight(suggestion))
225+
}
226+
}
227+
} else {
228+
// Fallback for non-Ork errors
229+
ui.Error(fmt.Sprintf("Error: %v", err))
230+
}
231+
}

internal/service/orchestrator.go

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/ork-cli/ork/internal/config"
1010
"github.com/ork-cli/ork/internal/docker"
11+
"github.com/ork-cli/ork/internal/ui"
1112
)
1213

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

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

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

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

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

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

205-
fmt.Printf("✅ Started %s (container: %s)\n", serviceName, svc.GetContainerID()[:12])
207+
containerID := svc.GetContainerID()
208+
if len(containerID) > 12 {
209+
containerID = containerID[:12]
210+
}
211+
spinner.Success(fmt.Sprintf("Started %s %s", ui.Bold(serviceName), ui.Dim(containerID)))
206212

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

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

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

281-
fmt.Printf("✅ %s is healthy\n", service.Name)
287+
ui.Success(fmt.Sprintf("%s is healthy", service.Name))
282288
}(svc)
283289
}
284290

@@ -344,17 +350,18 @@ func (o *Orchestrator) rollbackStartedServices(ctx context.Context, startedServi
344350
return
345351
}
346352

347-
fmt.Printf("🔄 Rolling back %d started service(s)...\n", len(startedServices))
353+
ui.EmptyLine()
354+
ui.Warning(fmt.Sprintf("Rolling back %d started service(s)...", len(startedServices)))
348355

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

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

389-
fmt.Printf("🛑 Stopping %s...\n", service.Name)
396+
spinner := ui.ShowSpinner(fmt.Sprintf("Stopping %s", service.Name))
390397
if err := service.Stop(ctx, o.dockerClient); err != nil {
398+
spinner.Error(fmt.Sprintf("Failed to stop %s", service.Name))
391399
errChan <- fmt.Errorf("failed to stop %s: %w", service.Name, err)
392400
return
393401
}
394-
fmt.Printf("✅ Stopped %s\n", service.Name)
402+
spinner.Success(fmt.Sprintf("Stopped %s", service.Name))
395403
}(svc)
396404
}
397405

0 commit comments

Comments
 (0)