Skip to content

Commit 7b2d034

Browse files
committed
refactor: restructure config with nested backend structs and centralized defaults
Split the Config god struct into Config (global) + Defaults ScaleSetConfig (squashed) with nested DockerConfig/TartConfig sub-structs. This replaces the flat tart-*/docker-* keys with [docker] and [tart] TOML tables. Key changes: - Add defaults.go as single source of truth for all default values - Separate Docker and Tart settings into DockerConfig/TartConfig structs - Convert Logger(), ScalesetClient(), BuildLabels() from struct methods to standalone functions (NewLogger, NewScalesetClient, BuildLabels) - Fix SystemInfo to accept version parameter instead of hardcoding "0.1.0" - Add --backend and Tart CLI flags (--tart-image, --tart-cpu, etc.) - Add health-port and dry-run to Config struct (now settable via TOML) - Extract shared loadConfig() helper, eliminating duplicate config loading - Make --config a persistent flag shared by root and validate commands - Set backend default to "docker" via applyDefaults() instead of "" - Use explicit viper.BindPFlag() for nested key mapping - Update README, config.example.toml, and tests for new TOML structure
1 parent 4d4c9d2 commit 7b2d034

File tree

12 files changed

+580
-387
lines changed

12 files changed

+580
-387
lines changed

README.md

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ flowchart LR
7878
| `ghcr.io/cirruslabs/macos-tahoe-xcode:latest` | 4 cores | 8 GB (8192 MB) | 120 GB |
7979
| `ghcr.io/cirruslabs/macos-sequoia-xcode:latest` | 4 cores | 8 GB (8192 MB) | 120 GB |
8080

81-
Override per VM with `tart-cpu` and `tart-memory` in config. For iOS builds (Xcode), 8 GB+ is recommended.
81+
Override per VM with `cpu` and `memory` under `[tart]` in config. For iOS builds (Xcode), 8 GB+ is recommended.
8282

8383
- A GitHub **Personal Access Token** — required scopes depend on token type and runner level:
8484

@@ -174,11 +174,13 @@ min-runners = 0
174174
labels = ["self-hosted", "linux"]
175175
runner-image = "ghcr.io/actions/actions-runner:latest"
176176
runner-group = "default"
177-
docker-socket = "/var/run/docker.sock"
178-
dind = true
179-
shared-volume = "/shared"
180177
log-level = "info"
181178
log-format = "text"
179+
180+
[docker]
181+
socket = "/var/run/docker.sock"
182+
dind = true
183+
shared-volume = "/shared"
182184
```
183185

184186
**Tart backend (macOS):**
@@ -191,12 +193,14 @@ name = "macos-runners"
191193
token = "ghp_xxx"
192194
max-runners = 2 # Apple limits 2 concurrent macOS VMs per host
193195
labels = ["self-hosted", "macOS"]
194-
tart-image = "ghcr.io/cirruslabs/macos-tahoe-xcode:latest"
195-
tart-cpu = 4 # CPU cores per VM (0 = use image default)
196-
tart-memory = 8192 # Memory in MB per VM (0 = use image default)
197-
tart-runner-dir = "/Users/admin/actions-runner" # default
198-
tart-pool-size = 2 # pre-warm 2 VMs for instant job pickup (~2s vs ~30s cold boot)
199196
log-level = "info"
197+
198+
[tart]
199+
image = "ghcr.io/cirruslabs/macos-tahoe-xcode:latest"
200+
cpu = 4 # CPU cores per VM (0 = use image default)
201+
memory = 8192 # Memory in MB per VM (0 = use image default)
202+
runner-dir = "/Users/admin/actions-runner" # default
203+
pool-size = 2 # pre-warm 2 VMs for instant job pickup (~2s vs ~30s cold boot)
200204
```
201205

202206
### Token Security
@@ -221,59 +225,62 @@ Priority: `--token` flag > `RUNSCALER_TOKEN` env var > config file value (includ
221225
**Multiple scale sets (mixed Docker + Tart):**
222226

223227
```toml
224-
# Global settings
228+
# Global defaults (inherited by all scale sets)
225229
runner-image = "ghcr.io/actions/actions-runner:latest"
226230
runner-group = "default"
227231
max-runners = 10
228232
log-level = "info"
229233
234+
[docker]
235+
socket = "/var/run/docker.sock"
236+
dind = true
237+
230238
# Each [[scaleset]] runs independently.
231239
# Inherits global settings if omitted.
232240
233241
[[scaleset]]
234242
url = "https://github.com/your-org"
235243
name = "linux-runners"
236244
token = "ghp_aaa"
237-
docker-socket = "/var/run/docker.sock"
238-
dind = true
239245
240246
[[scaleset]]
241247
backend = "tart"
242248
url = "https://github.com/your-org"
243249
name = "macos-runners"
244250
token = "ghp_bbb"
245-
tart-image = "ghcr.io/cirruslabs/macos-tahoe-xcode:latest"
246251
max-runners = 2
247252
labels = ["self-hosted", "macOS"]
248-
tart-pool-size = 2
253+
[scaleset.tart]
254+
image = "ghcr.io/cirruslabs/macos-tahoe-xcode:latest"
255+
pool-size = 2
249256
```
250257

251258
### CLI Flags
252259

253-
| Flag | Default | Description |
254-
| ------------------- | --------------------------------------- | ------------------------------------------------- |
255-
| `--config` | | Path to TOML config file |
256-
| `--url` | (required) | Registration URL (org or repo) |
257-
| `--name` | (required) | Scale set name (used as `runs-on` label) |
258-
| `--token` | (required) | GitHub Personal Access Token |
259-
| `--backend` | `docker` | Runner backend (`docker` or `tart`) |
260-
| `--max-runners` | `10` | Maximum concurrent runners |
261-
| `--min-runners` | `0` | Minimum runners to keep warm |
262-
| `--labels` | `<name>` | Runner labels (comma-separated) |
263-
| `--runner-group` | `default` | Runner group name |
264-
| `--runner-image` | `ghcr.io/actions/actions-runner:latest` | Docker image (Docker backend) |
265-
| `--docker-socket` | `/var/run/docker.sock` | Docker socket path (Docker backend) |
266-
| `--dind` | `true` | Mount Docker socket into runners (Docker backend) |
267-
| `--shared-volume` | | Shared Docker volume path (Docker backend) |
268-
| `--tart-image` | | Tart VM image name (Tart backend, required) |
269-
| `--tart-cpu` | `0` (image default) | CPU cores per VM (Tart backend) |
270-
| `--tart-memory` | `0` (image default) | Memory in MB per VM (Tart backend) |
271-
| `--tart-runner-dir` | `/Users/admin/actions-runner` | Runner install directory inside Tart VM |
272-
| `--tart-pool-size` | `0` | Number of pre-warmed VMs for instant job pickup |
273-
| `--log-level` | `info` | Log level (debug/info/warn/error) |
274-
| `--log-format` | `text` | Log format (text/json) |
275-
| `--dry-run` | `false` | Validate everything without starting listeners |
276-
| `--health-port` | `8080` | Health check HTTP port (0 to disable) |
260+
| Flag | TOML key | Default | Description |
261+
| ------------------- | -------------------- | --------------------------------------- | ------------------------------------------------- |
262+
| `--config` | | | Path to TOML config file |
263+
| `--url` | `url` | (required) | Registration URL (org or repo) |
264+
| `--name` | `name` | (required) | Scale set name (used as `runs-on` label) |
265+
| `--token` | `token` | (required) | GitHub Personal Access Token |
266+
| `--backend` | `backend` | `docker` | Runner backend (`docker` or `tart`) |
267+
| `--max-runners` | `max-runners` | `10` | Maximum concurrent runners |
268+
| `--min-runners` | `min-runners` | `0` | Minimum runners to keep warm |
269+
| `--labels` | `labels` | `<name>` | Runner labels (comma-separated) |
270+
| `--runner-group` | `runner-group` | `default` | Runner group name |
271+
| `--runner-image` | `runner-image` | `ghcr.io/actions/actions-runner:latest` | Docker image (Docker backend) |
272+
| `--docker-socket` | `[docker] socket` | `/var/run/docker.sock` | Docker socket path (Docker backend) |
273+
| `--dind` | `[docker] dind` | `true` | Mount Docker socket into runners (Docker backend) |
274+
| `--shared-volume` | `[docker] shared-volume` | | Shared Docker volume path (Docker backend) |
275+
| `--tart-image` | `[tart] image` | | Tart VM image name (Tart backend, required) |
276+
| `--tart-cpu` | `[tart] cpu` | `0` (image default) | CPU cores per VM (Tart backend) |
277+
| `--tart-memory` | `[tart] memory` | `0` (image default) | Memory in MB per VM (Tart backend) |
278+
| `--tart-runner-dir` | `[tart] runner-dir` | `/Users/admin/actions-runner` | Runner install directory inside Tart VM |
279+
| `--tart-pool-size` | `[tart] pool-size` | `0` | Number of pre-warmed VMs for instant job pickup |
280+
| `--log-level` | `log-level` | `info` | Log level (debug/info/warn/error) |
281+
| `--log-format` | `log-format` | `text` | Log format (text/json) |
282+
| `--dry-run` | `dry-run` | `false` | Validate everything without starting listeners |
283+
| `--health-port` | `health-port` | `8080` | Health check HTTP port (0 to disable) |
277284

278285
## Deployment
279286

cmd/runscaler/cmd_init.go

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99

1010
"github.com/spf13/cobra"
1111
"golang.org/x/term"
12+
13+
"github.com/ysya/runscaler/internal/config"
1214
)
1315

1416
var initCmd = &cobra.Command{
@@ -28,9 +30,9 @@ func init() {
2830
flags.String("url", "", "Registration URL (e.g. https://github.com/org)")
2931
flags.String("name", "", "Scale set name")
3032
flags.String("token", "", "Personal access token")
31-
flags.Int("max-runners", 10, "Maximum concurrent runners")
33+
flags.Int("max-runners", config.DefaultMaxRunners, "Maximum concurrent runners")
3234
flags.String("backend", "", "Runner backend (docker or tart)")
33-
flags.Bool("dind", true, "Enable Docker-in-Docker")
35+
flags.Bool("dind", config.DefaultDinD, "Enable Docker-in-Docker")
3436
flags.String("shared-volume", "", "Shared volume path (e.g. /shared)")
3537
flags.String("tart-image", "", "Base Tart VM image for macOS runners")
3638
flags.String("output", "config.toml", "Output file path")
@@ -80,7 +82,7 @@ func runInit(cmd *cobra.Command, args []string) error {
8082
}
8183
}
8284
if !cmd.Flags().Changed("max-runners") {
83-
maxRunners, err = promptInt("Maximum concurrent runners", 10)
85+
maxRunners, err = promptInt("Maximum concurrent runners", config.DefaultMaxRunners)
8486
if err != nil {
8587
return err
8688
}
@@ -95,7 +97,7 @@ func runInit(cmd *cobra.Command, args []string) error {
9597
if useTart {
9698
backend = "tart"
9799
} else {
98-
backend = "docker"
100+
backend = config.DefaultBackend
99101
}
100102
}
101103

@@ -131,23 +133,33 @@ min-runners = 0
131133
# Backend: "docker" (Linux containers) or "tart" (macOS VMs)
132134
backend = "tart"
133135
136+
[tart]
134137
# Base Tart VM image (must have GitHub Actions runner pre-installed)
135-
tart-image = %q
138+
image = %q
136139
137140
# Path to the runner binary inside the VM
138-
tart-runner-dir = "/Users/admin/actions-runner"
141+
runner-dir = %q
139142
140143
# Logging
141-
log-level = "info"
142-
log-format = "text"
144+
[docker]
145+
# Not used with tart backend, but shown for reference
146+
# socket = %q
147+
148+
# --- Global ---
149+
log-level = %q
150+
log-format = %q
143151
144152
# Health check server port (0 to disable)
145-
# health-port = 8080
146-
`, url, name, token, maxRunners, tartImage)
153+
# health-port = %d
154+
`, url, name, token, maxRunners,
155+
tartImage, config.DefaultTartRunnerDir,
156+
config.DefaultDockerSocket,
157+
config.DefaultLogLevel, config.DefaultLogFormat,
158+
config.DefaultHealthPort)
147159
} else {
148160
// Docker backend config
149161
if !cmd.Flags().Changed("dind") {
150-
dind, err = promptYN("Enable Docker-in-Docker?", true)
162+
dind, err = promptYN("Enable Docker-in-Docker?", config.DefaultDinD)
151163
if err != nil {
152164
return err
153165
}
@@ -181,23 +193,27 @@ max-runners = %d
181193
min-runners = 0
182194
183195
# Docker image for runners
184-
runner-image = "ghcr.io/actions/actions-runner:latest"
196+
runner-image = %q
185197
198+
# Backend: "docker" (Linux containers) or "tart" (macOS VMs)
199+
backend = %q
200+
201+
[docker]
186202
# Docker-in-Docker: mount host Docker socket into runners
187203
dind = %v
188204
189205
# Docker socket path
190-
docker-socket = "/var/run/docker.sock"
206+
socket = %q
191207
192208
# Shared volume for cross-job data sharing (optional)
193209
shared-volume = %q
194210
195-
# Logging
196-
log-level = "info"
197-
log-format = "text"
211+
# --- Global ---
212+
log-level = %q
213+
log-format = %q
198214
199215
# Health check server port (0 to disable)
200-
# health-port = 8080
216+
# health-port = %d
201217
202218
# --- Multi-org / mixed backend example ---
203219
# Uncomment and duplicate [[scaleset]] blocks:
@@ -214,9 +230,14 @@ log-format = "text"
214230
# name = "macos-runners"
215231
# token = "env:TOKEN_ORG_A"
216232
# backend = "tart"
217-
# tart-image = "ghcr.io/cirruslabs/macos-sequoia-xcode:latest"
218233
# max-runners = 2
219-
`, url, name, token, maxRunners, dind, sharedVolume)
234+
# [scaleset.tart]
235+
# image = "ghcr.io/cirruslabs/macos-sequoia-xcode:latest"
236+
`, url, name, token, maxRunners,
237+
config.DefaultRunnerImage, config.DefaultBackend,
238+
dind, config.DefaultDockerSocket, sharedVolume,
239+
config.DefaultLogLevel, config.DefaultLogFormat,
240+
config.DefaultHealthPort)
220241
}
221242

222243
if err := os.WriteFile(output, []byte(configContent), 0600); err != nil {

cmd/runscaler/cmd_status.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/spf13/cobra"
1212

13+
"github.com/ysya/runscaler/internal/config"
1314
"github.com/ysya/runscaler/internal/health"
1415
)
1516

@@ -25,7 +26,7 @@ var statusCmd = &cobra.Command{
2526

2627
func init() {
2728
flags := statusCmd.Flags()
28-
flags.Int("health-port", 8080, "Health check server port to connect to")
29+
flags.Int("health-port", config.DefaultHealthPort, "Health check server port to connect to")
2930
flags.Bool("json", false, "Output raw JSON")
3031
}
3132

cmd/runscaler/cmd_validate.go

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

99
dockerclient "github.com/docker/docker/client"
1010
"github.com/spf13/cobra"
11-
"github.com/spf13/viper"
1211

1312
"github.com/ysya/runscaler/internal/config"
1413
)
@@ -17,32 +16,18 @@ var validateCmd = &cobra.Command{
1716
Use: "validate",
1817
Short: "Validate configuration and connectivity",
1918
Long: "Check that the config file is valid, Docker/Tart is reachable, and GitHub tokens work.",
20-
Example: ` runscaler validate --config config.toml
21-
runscaler validate --url https://github.com/org --name test --token ghp_xxx`,
19+
Example: ` runscaler validate --config config.toml`,
2220
RunE: runValidate,
2321
}
2422

25-
func init() {
26-
flags := validateCmd.Flags()
27-
flags.String("config", "", "Path to config file (TOML)")
28-
}
29-
3023
func runValidate(cmd *cobra.Command, args []string) error {
31-
// Load config using same logic as root command
32-
if configFile, _ := cmd.Flags().GetString("config"); configFile != "" {
33-
viper.SetConfigFile(configFile)
34-
if err := viper.ReadInConfig(); err != nil {
35-
return fmt.Errorf("failed to read config file: %w", err)
36-
}
37-
}
38-
39-
var c config.Config
40-
if err := viper.Unmarshal(&c); err != nil {
41-
return fmt.Errorf("failed to parse configuration: %w", err)
24+
cfg, err := loadConfig(cmd)
25+
if err != nil {
26+
return err
4227
}
4328

4429
// Validate scale sets
45-
scaleSets := c.ResolveScaleSets()
30+
scaleSets := cfg.ResolveScaleSets()
4631
if len(scaleSets) == 0 {
4732
return fmt.Errorf("no scale sets configured")
4833
}
@@ -52,12 +37,8 @@ func runValidate(cmd *cobra.Command, args []string) error {
5237
fmt.Printf(" ✗ scaleset[%d] %q: %s\n", i, scaleSets[i].ScaleSetName, err)
5338
return fmt.Errorf("validation failed")
5439
}
55-
b := scaleSets[i].Backend
56-
if b == "" {
57-
b = "docker"
58-
}
5940
fmt.Printf(" ✓ scaleset[%d] %q — backend=%s url=%s max=%d min=%d\n",
60-
i, scaleSets[i].ScaleSetName, b, scaleSets[i].RegistrationURL,
41+
i, scaleSets[i].ScaleSetName, scaleSets[i].Backend, scaleSets[i].RegistrationURL,
6142
scaleSets[i].MaxRunners, scaleSets[i].MinRunners,
6243
)
6344
}
@@ -95,7 +76,7 @@ func runValidate(cmd *cobra.Command, args []string) error {
9576
fmt.Println(" 3. Re-login or run: newgrp docker")
9677
return fmt.Errorf("validation failed")
9778
}
98-
fmt.Printf(" ✓ Docker is reachable at %s\n", c.DockerSocket)
79+
fmt.Printf(" ✓ Docker is reachable at %s\n", cfg.Defaults.Docker.Socket)
9980
}
10081

10182
// Test Tart binary (only if needed)
@@ -116,16 +97,16 @@ func runValidate(cmd *cobra.Command, args []string) error {
11697
}
11798

11899
// Show shared volume status
119-
if c.SharedVolume != "" {
120-
fmt.Printf(" ✓ Shared volume enabled at %s\n", c.SharedVolume)
100+
if cfg.Defaults.Docker.SharedVolume != "" {
101+
fmt.Printf(" ✓ Shared volume enabled at %s\n", cfg.Defaults.Docker.SharedVolume)
121102
} else if needsDocker {
122103
fmt.Println(" - Shared volume: not configured (cross-job sharing will not work)")
123104
}
124105

125106
// Test GitHub API connectivity for each scale set
107+
logger := config.NewLogger(cfg.LogLevel, cfg.LogFormat)
126108
for i, ss := range scaleSets {
127-
logger := c.Logger()
128-
client, err := ss.ScalesetClient(logger)
109+
client, err := config.NewScalesetClient(ss.RegistrationURL, ss.Token, logger)
129110
if err != nil {
130111
fmt.Printf(" ✗ scaleset[%d] %q GitHub API: %s\n", i, ss.ScaleSetName, err)
131112
return fmt.Errorf("validation failed")

0 commit comments

Comments
 (0)