Skip to content

Commit 1ac9578

Browse files
committed
feat: add up orchestrator command and document end-to-end lab workflow
**Added:** - Introduced `up` command for end-to-end lab deployment, orchestrating doctor, infra apply, provision, and health-check steps with flags for granular control (`cli/cmd/up.go`) - Expanded CLI reference with detailed `init` and `up` sections, including usage examples and flag descriptions (`docs/mkdocs/docs/cli-reference.md`) - Added quickstart documentation highlighting the new orchestrator workflow and resume mechanism (`docs/mkdocs/docs/usage/index.md`) - Documented orchestrator workflow for Ludus, with usage and resume examples (`docs/mkdocs/docs/providers/ludus.md`)
1 parent 3ac3c17 commit 1ac9578

4 files changed

Lines changed: 252 additions & 2 deletions

File tree

‎cli/cmd/up.go‎

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"strings"
7+
"time"
8+
9+
"github.com/dreadnode/dreadgoad/internal/config"
10+
"github.com/dreadnode/dreadgoad/internal/doctor"
11+
"github.com/fatih/color"
12+
"github.com/spf13/cobra"
13+
)
14+
15+
var (
16+
upSkipDoctor bool
17+
upFromStep string
18+
upLimit string
19+
upPlays string
20+
upMaxRetries int
21+
upRetryDelay int
22+
upInfraModule string
23+
upInfraExclude string
24+
)
25+
26+
var upCmd = &cobra.Command{
27+
Use: "up",
28+
Short: "Deploy the lab end-to-end (doctor → infra → provision → health-check)",
29+
Long: `One-command lab bring-up. Runs the full pipeline in order:
30+
31+
1. doctor pre-flight tooling and connectivity checks
32+
2. infra apply provision instances/range (auto-approved)
33+
3. provision run Ansible playbooks to build AD
34+
4. health-check verify DCs, replication, trusts, services
35+
36+
Stops on the first failing step and prints a resume hint. Use --from <step>
37+
to restart from a specific point. The recommended new-user flow is:
38+
39+
dreadgoad init && dreadgoad up`,
40+
Example: ` dreadgoad up
41+
dreadgoad up --skip-doctor
42+
dreadgoad up --from provision
43+
dreadgoad up --limit dc01`,
44+
RunE: runUp,
45+
}
46+
47+
func init() {
48+
rootCmd.AddCommand(upCmd)
49+
50+
upCmd.Flags().BoolVar(&upSkipDoctor, "skip-doctor", false, "Skip the doctor pre-flight checks")
51+
upCmd.Flags().StringVar(&upFromStep, "from", "", "Resume from this step (doctor, infra, provision, health-check)")
52+
upCmd.Flags().StringVar(&upLimit, "limit", "", "Limit provisioning to specific hosts")
53+
upCmd.Flags().StringVar(&upPlays, "plays", "", "Comma-separated playbooks to run (default: all)")
54+
upCmd.Flags().IntVar(&upMaxRetries, "max-retries", 0, "Max retry attempts for provisioning")
55+
upCmd.Flags().IntVar(&upRetryDelay, "retry-delay", 0, "Delay between retries in seconds")
56+
upCmd.Flags().StringVar(&upInfraModule, "module", "", "Target a specific infra module (default: all)")
57+
upCmd.Flags().StringVar(&upInfraExclude, "exclude", "", "Exclude infra modules (comma-separated)")
58+
}
59+
60+
type upStep struct {
61+
id string
62+
name string
63+
run func(cmd *cobra.Command, args []string) error
64+
}
65+
66+
func runUp(cmd *cobra.Command, args []string) error {
67+
steps := []upStep{
68+
{id: "doctor", name: "Pre-flight checks", run: runUpDoctor},
69+
{id: "infra", name: "Infrastructure apply", run: runUpInfraApply},
70+
{id: "provision", name: "Configuration provisioning", run: runUpProvision},
71+
{id: "health-check", name: "Lab health check", run: runUpHealthCheck},
72+
}
73+
74+
if upFromStep != "" {
75+
idx := -1
76+
for i, s := range steps {
77+
if s.id == upFromStep {
78+
idx = i
79+
break
80+
}
81+
}
82+
if idx < 0 {
83+
valid := make([]string, len(steps))
84+
for i, s := range steps {
85+
valid[i] = s.id
86+
}
87+
return fmt.Errorf("--from %q is not a valid step (one of: %s)", upFromStep, strings.Join(valid, ", "))
88+
}
89+
steps = steps[idx:]
90+
} else if upSkipDoctor {
91+
steps = steps[1:]
92+
}
93+
94+
total := len(steps)
95+
start := time.Now()
96+
for i, step := range steps {
97+
printUpHeader(i+1, total, step.name)
98+
if err := step.run(cmd, args); err != nil {
99+
fmt.Println()
100+
color.Red("✗ %s failed: %v", step.name, err)
101+
color.Yellow(" Resume with: dreadgoad up --from %s", step.id)
102+
return err
103+
}
104+
}
105+
106+
fmt.Println()
107+
color.Green("✓ Lab is up. Total time: %s", time.Since(start).Round(time.Second))
108+
fmt.Println("Next: dreadgoad validate # vulnerability checks")
109+
return nil
110+
}
111+
112+
func printUpHeader(step, total int, name string) {
113+
line := strings.Repeat("━", 60)
114+
fmt.Println()
115+
color.Cyan(line)
116+
color.Cyan("▶ Step %d/%d %s", step, total, name)
117+
color.Cyan(line)
118+
}
119+
120+
func runUpDoctor(cmd *cobra.Command, _ []string) error {
121+
cfg, err := config.Get()
122+
if err != nil {
123+
return err
124+
}
125+
results := doctor.RunChecks(doctor.Options{
126+
InventoryPath: cfg.InventoryPath(),
127+
ProjectRoot: cfg.ProjectRoot,
128+
Provider: cfg.ResolvedProvider(),
129+
Ludus: doctor.LudusOptions{
130+
APIKey: cfg.Ludus.APIKey,
131+
SSHHost: cfg.Ludus.SSHTarget(),
132+
SSHUser: cfg.Ludus.SSHUser,
133+
SSHKeyPath: cfg.Ludus.SSHKeyPath,
134+
SSHPassword: cfg.Ludus.SSHPassword,
135+
SSHPort: cfg.Ludus.SSHPort,
136+
ResolveAlias: cfg.Ludus.Host != "" &&
137+
cfg.Ludus.SSHUser == "" &&
138+
cfg.Ludus.SSHKeyPath == "" &&
139+
cfg.Ludus.SSHPassword == "" &&
140+
cfg.Ludus.SSHPort == 0,
141+
},
142+
})
143+
doctor.PrintResults(results)
144+
for _, r := range results {
145+
if r.Status == "fail" {
146+
return fmt.Errorf("one or more pre-flight checks failed (re-run 'dreadgoad doctor' for details, or pass --skip-doctor to bypass)")
147+
}
148+
}
149+
return nil
150+
}
151+
152+
// runUpInfraApply invokes `infra apply` with auto-approve. We build a
153+
// synthetic cobra.Command so the inner action sees only the flags we want
154+
// (auto-approve=true, module/exclude pass-through) without conflating with
155+
// the up command's own flag set.
156+
func runUpInfraApply(cmd *cobra.Command, args []string) error {
157+
infraCmd := &cobra.Command{}
158+
infraCmd.Flags().String("module", upInfraModule, "")
159+
infraCmd.Flags().String("exclude", upInfraExclude, "")
160+
infraCmd.Flags().Bool("auto-approve", true, "")
161+
infraCmd.Flags().Bool("individual", false, "")
162+
infraCmd.Flags().String("deployment", "", "")
163+
infraCmd.SetContext(cmd.Context())
164+
return runInfraAction("apply")(infraCmd, args)
165+
}
166+
167+
// runUpProvision calls runProvision via a synthetic command so up's --from
168+
// (which is the step name) is not mistakenly read as the playbook-resume
169+
// flag of the provision subcommand.
170+
func runUpProvision(cmd *cobra.Command, args []string) error {
171+
provCmd := &cobra.Command{}
172+
provCmd.Flags().String("plays", "", "")
173+
provCmd.Flags().String("from", "", "")
174+
provCmd.Flags().String("limit", "", "")
175+
provCmd.Flags().Int("max-retries", 0, "")
176+
provCmd.Flags().Int("retry-delay", 0, "")
177+
if upPlays != "" {
178+
_ = provCmd.Flags().Set("plays", upPlays)
179+
}
180+
if upLimit != "" {
181+
_ = provCmd.Flags().Set("limit", upLimit)
182+
}
183+
if upMaxRetries > 0 {
184+
_ = provCmd.Flags().Set("max-retries", strconv.Itoa(upMaxRetries))
185+
}
186+
if upRetryDelay > 0 {
187+
_ = provCmd.Flags().Set("retry-delay", strconv.Itoa(upRetryDelay))
188+
}
189+
provCmd.SetContext(cmd.Context())
190+
return runProvision(provCmd, args)
191+
}
192+
193+
func runUpHealthCheck(cmd *cobra.Command, args []string) error {
194+
return runHealthCheck(cmd, args)
195+
}

‎docs/mkdocs/docs/cli-reference.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,38 @@ vulnerable Active Directory environments for security research and testing.
1919

2020
## Getting Started
2121

22+
### init
23+
24+
Interactive setup wizard. Walks through provider selection and connectivity probing, then writes a working `dreadgoad.yaml`.
25+
26+
```bash
27+
dreadgoad init
28+
```
29+
30+
For Ludus the wizard prefers an `~/.ssh/config` Host alias so existing key/agent setups (including 1Password) carry through with zero extra configuration.
31+
32+
### up
33+
34+
Deploy the lab end-to-end: doctor → infra apply → provision → health-check. Stops on the first failing step and prints a resume hint.
35+
36+
```bash
37+
dreadgoad up # full pipeline
38+
dreadgoad up --from provision # resume from a step
39+
dreadgoad up --skip-doctor # bypass pre-flight checks
40+
dreadgoad up --limit dc01 # narrow provisioning to one host
41+
```
42+
43+
| Flag | Description |
44+
|------|-------------|
45+
| `--from string` | Resume from this step (`doctor`, `infra`, `provision`, `health-check`) |
46+
| `--skip-doctor` | Skip the doctor pre-flight checks |
47+
| `--limit string` | Limit provisioning to specific hosts |
48+
| `--plays string` | Comma-separated playbooks to run (default: all) |
49+
| `--max-retries int` | Max retry attempts for provisioning |
50+
| `--retry-delay int` | Delay between retries in seconds |
51+
| `--module string` | Target a specific infra module |
52+
| `--exclude string` | Exclude infra modules (comma-separated) |
53+
2254
### config
2355

2456
Manage CLI configuration.

‎docs/mkdocs/docs/providers/ludus.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,20 @@ Setting any of `ssh_user`/`ssh_port`/`ssh_key_path`/`ssh_password` switches Drea
137137

138138
## Installation
139139

140+
The fastest path is the orchestrator — `dreadgoad up` runs the whole pipeline (doctor → infra apply → provision → health-check) and stops on the first failure with a resume hint:
141+
142+
```bash
143+
dreadgoad up
144+
```
145+
146+
If a step fails, fix the issue and re-run from there:
147+
148+
```bash
149+
dreadgoad up --from provision
150+
```
151+
152+
The orchestrator is just sugar for the per-step commands, which still work standalone:
153+
140154
```bash
141155
# Check prerequisites
142156
dreadgoad --provider ludus doctor

‎docs/mkdocs/docs/usage/index.md‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,24 @@ See the [CLI Reference](../cli-reference.md) for the full command listing.
1010

1111
## Common workflows
1212

13-
### First-time setup
13+
### Quickstart (recommended)
14+
15+
```bash
16+
dreadgoad init # Interactive wizard — writes dreadgoad.yaml
17+
dreadgoad up # doctor → infra apply → provision → health-check
18+
```
19+
20+
`up` stops on the first failing step and prints a resume hint (`dreadgoad up --from <step>`).
21+
22+
### First-time setup (manual)
1423

1524
```bash
1625
dreadgoad config init # Create default config
1726
dreadgoad doctor # Verify dependencies
1827
dreadgoad env create dev # Create an environment
1928
```
2029

21-
### Deploy a lab
30+
### Deploy a lab (per-step)
2231

2332
```bash
2433
dreadgoad infra init # Initialize Terragrunt

0 commit comments

Comments
 (0)