-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbooth_runner.go
More file actions
158 lines (132 loc) · 4.9 KB
/
booth_runner.go
File metadata and controls
158 lines (132 loc) · 4.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
// Copyright 2025-2026 : Nawa Manusitthipol
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// Package booth provides the main Booth type for managing Docker-based development environments.
package booth
import (
"fmt"
"os"
"strings"
"github.com/nawaman/codingbooth/src/pkg/appctx"
"github.com/nawaman/codingbooth/src/pkg/docker"
"github.com/nawaman/codingbooth/src/pkg/ilist"
)
// BoothRunner handles the "run" command for booth operations.
// It orchestrates the preparation of AppContext and execution of the booth.
type BoothRunner struct {
ctx appctx.AppContext
}
// NewBoothRunner creates a new BoothRunner with the given AppContext.
func NewBoothRunner(ctx appctx.AppContext) *BoothRunner {
return &BoothRunner{ctx: ctx}
}
// Run is the main entry point that prepares the context and executes the booth.
func (runner *BoothRunner) Run() error {
// Prepare arguments and determine run mode (matching booth order)
ctx := runner.ctx
SetLogTime(ctx.LogTime())
ctx = ValidateVariant(ctx)
ctx = EnsureDockerImage(ctx)
ctx = PortDetermination(ctx)
ctx = ResolveRelativePorts(ctx)
ctx = ShowDebugBanner(ctx)
ctx = SetupDind(ctx)
ctx = SetupSandbox(ctx)
ctx = PrepareRunMode(ctx)
ctx = FilterMissingVolumeMounts(ctx)
ctx = PrepareBoothTmp(ctx)
// ApplyEnvFile runs after PrepareBoothTmp so the expanded env file we
// write into .booth/.tmp/ is not wiped by the start-of-run cleanup.
ctx = ApplyEnvFile(ctx)
ctx = PrepareCommonArgs(ctx)
if err := ensureContainerNameAvailable(ctx); err != nil {
return err
}
// Create booth with prepared context and run
booth := NewBooth(ctx)
return booth.Run(ctx.RunMode())
}
func ensureContainerNameAvailable(ctx appctx.AppContext) error {
flags := docker.DockerFlags{
Dryrun: ctx.Dryrun(),
Verbose: ctx.Verbose(),
Silent: true,
}
containerName := ctx.Name()
if containerName == "" {
containerName = ctx.ProjectName()
}
output, err := docker.DockerOutput(flags, "ps", ilist.NewList(
ilist.NewList("-a"),
ilist.NewList("--filter", "name=^"+containerName+"$"),
ilist.NewList("--format", "{{.Names}}"),
))
if err != nil {
return fmt.Errorf("failed to check container name availability: %w", err)
}
if strings.TrimSpace(output) == "" {
return nil
}
return fmt.Errorf(
"container name %q already exists. Choose a different name with --name, or remove the existing one with 'booth remove --name %s' (or 'docker rm -f %s')",
containerName,
containerName,
containerName,
)
}
// PrepareRunMode determines the run mode and stores it in the context.
func PrepareRunMode(ctx appctx.AppContext) appctx.AppContext {
builder := ctx.ToBuilder()
if ctx.Daemon() {
builder.RunMode = "DAEMON"
} else if ctx.Cmds().Length() == 0 {
builder.RunMode = "FOREGROUND"
} else {
builder.RunMode = "COMMAND"
}
return builder.Build()
}
// SetupDind sets up Docker-in-Docker if enabled and returns updated AppContext.
func SetupDind(ctx appctx.AppContext) appctx.AppContext {
// Early return if DinD is not enabled
if !ctx.Dind() {
return ctx
}
builder := ctx.ToBuilder()
// Clean up any leftover containers/networks from previous booth runs
// This prevents port conflicts when restarting the booth
cleanupPreviousBoothInstances(ctx, ctx.ProjectName())
// Set up unique network and sidecar names
dindNet := getDindNet(ctx)
dindName := getDindName(ctx)
// Create network if it doesn't exist
createdNet := createDindNetwork(ctx, dindNet)
builder.CreatedDindNet = createdNet
// Extract extra port mappings from RunArgs before stripping
extraPorts := extractPortFlags(ctx.RunArgs())
// Start DinD sidecar if not already running (pass hostPort for port mapping)
err := startDindSidecar(ctx, dindName, dindNet, ctx.PortNumber(), extraPorts)
if err != nil {
fmt.Fprintf(os.Stderr, "❌ Failed to start DinD sidecar.\n\n")
// Try to diagnose if this is a port conflict
port, diagnostic := diagnosePortConflict(err, ctx.PortNumber(), extraPorts)
if port != "" {
fmt.Fprintf(os.Stderr, " %s\n", diagnostic)
} else {
// Generic error - show the original error message
fmt.Fprintf(os.Stderr, " Error: %v\n", err)
fmt.Fprintf(os.Stderr, " Check if any port is already in use.\n")
fmt.Fprintf(os.Stderr, " Use 'lsof -i :<port>' or 'ss -tlnp | grep <port>' to find the process.\n")
}
os.Exit(1)
}
// Wait for DinD to become ready
waitForDindReady(ctx, dindName, dindNet)
// Strip network and port flags from RUN_ARGS (not allowed with container network mode)
builder.RunArgs = stripNetworkAndPortFlags(ctx.RunArgs())
// Use container network mode to share DinD's network namespace
// This allows localhost access to DinD's ports from the booth
builder.CommonArgs.Append(ilist.NewList[string]("--network", fmt.Sprintf("container:%s", dindName)))
builder.CommonArgs.Append(ilist.NewList[string]("-e", "DOCKER_HOST=tcp://localhost:2375"))
return builder.Build()
}