Skip to content

Commit 298e2af

Browse files
committed
feat: add doctor command for local preflight checks and update Makefile with new targets
1 parent 191a55e commit 298e2af

4 files changed

Lines changed: 146 additions & 1 deletion

File tree

Makefile

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
# Makefile for running tests and building the C++ streamer locally
22

3-
.PHONY: test test-fast test-full build-clib
3+
CONFIG ?= configs/example-node.yaml
4+
5+
.PHONY: test test-fast test-full build build-clib check doctor
46

57
# Run Go unit tests quickly (no cgo streamer)
68
test-fast:
79
go test ./... -v
810

11+
# Build the Go CLI
12+
build:
13+
go build -v ./cmd/gs
14+
15+
# Run the standard fast validation flow
16+
check: test-fast build
17+
18+
# Run local preflight checks for the selected config
19+
doctor:
20+
go run ./cmd/gs doctor --config $(CONFIG)
21+
922
# Build C++ streamer and run Go tests with cgo_streamer enabled
1023
test-full: build-clib
1124
go test ./... -v -tags cgo_streamer

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ Runs 12 security checks across system, docker, nomad. Outputs alerts with severi
8686
./gs status 192.168.1.100
8787
```
8888

89+
**Doctor** - Local preflight checks
90+
```bash
91+
./gs doctor configs/example-node.yaml
92+
```
93+
Validates the local toolchain, config parsing, and optional SSH readiness.
94+
8995
## How Audit Works
9096

9197
Connects via SSH, runs bash commands on target, parses output to grade each check.
@@ -196,6 +202,13 @@ make build-c++ # C++ streamer library
196202

197203
Binary goes to ./gs
198204

205+
Workflow shortcuts:
206+
```bash
207+
make check # go test + build
208+
make doctor # run preflight checks for CONFIG=configs/example-node.yaml
209+
make test-full # build C++ streamer and run cgo-enabled tests
210+
```
211+
199212
## Performance
200213

201214
Extraction: 50-100 MB/s (database reading), 5-10% CPU overhead

cmd/gs/main.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"fmt"
55
"log/slog"
66
"os"
7+
"os/exec"
8+
"strings"
79

810
"github.com/matvejefimovyh/ghost-ship/internal/audit"
911
"github.com/matvejefimovyh/ghost-ship/internal/config"
@@ -69,6 +71,10 @@ func main() {
6971
}
7072
cmdStatus(os.Args[2])
7173
}
74+
case "doctor":
75+
{
76+
cmdDoctor(os.Args[2:])
77+
}
7278
default:
7379
fmt.Printf("Unknown command: %s\n", command)
7480
printUsage()
@@ -84,13 +90,15 @@ Available commands:
8490
migrate <from> <to> - Migrate infrastructure from one server to another
8591
audit <IP> - Run comprehensive security audit
8692
status <IP> - Check node status
93+
doctor [config] - Run local preflight checks before deployment
8794
8895
Examples:
8996
gs land 192.168.1.100 configs/relay-ru.yaml
9097
gs extract configs/relay-ru.yaml
9198
gs migrate --from 192.168.1.100 --to 192.168.1.101
9299
gs audit 192.168.1.100
93100
gs status 192.168.1.100
101+
gs doctor configs/example-node.yaml
94102
`)
95103
}
96104

@@ -267,6 +275,117 @@ func cmdStatus(ip string) {
267275
fmt.Println("Status command - not yet implemented")
268276
}
269277

278+
type doctorCheck struct {
279+
name string
280+
ok bool
281+
detail string
282+
}
283+
284+
func cmdDoctor(args []string) {
285+
configFile := "configs/example-node.yaml"
286+
targetIP := ""
287+
288+
for i := 0; i < len(args); i++ {
289+
switch args[i] {
290+
case "--config":
291+
if i+1 < len(args) {
292+
configFile = args[i+1]
293+
i++
294+
}
295+
case "--ip":
296+
if i+1 < len(args) {
297+
targetIP = args[i+1]
298+
i++
299+
}
300+
default:
301+
if !strings.HasPrefix(args[i], "-") && configFile == "configs/example-node.yaml" {
302+
configFile = args[i]
303+
}
304+
}
305+
}
306+
307+
checks := make([]doctorCheck, 0, 8)
308+
addCheck := func(name string, ok bool, detail string) {
309+
checks = append(checks, doctorCheck{name: name, ok: ok, detail: detail})
310+
}
311+
312+
addCheck("go toolchain", commandAvailable("go"), commandDetail("go"))
313+
addCheck("cmake", commandAvailable("cmake"), commandDetail("cmake"))
314+
addCheck("git", commandAvailable("git"), commandDetail("git"))
315+
addCheck("C++ compiler", commandAvailable("g++") || commandAvailable("clang++"), "g++ or clang++ must be available for the C++ streamer")
316+
317+
sshKey := os.Getenv("SSH_KEY")
318+
if sshKey == "" {
319+
addCheck("SSH key", false, "SSH_KEY is not set")
320+
} else if _, err := os.Stat(sshKey); err != nil {
321+
addCheck("SSH key", false, fmt.Sprintf("SSH_KEY points to an unreadable file: %v", err))
322+
} else {
323+
addCheck("SSH key", true, fmt.Sprintf("found at %s", sshKey))
324+
}
325+
326+
cfg, err := config.Load(configFile)
327+
if err != nil {
328+
addCheck("config", false, err.Error())
329+
} else {
330+
addCheck("config", true, fmt.Sprintf("loaded %s for %s (%s)", configFile, cfg.Node.IP, cfg.Node.Role))
331+
if targetIP == "" {
332+
targetIP = cfg.Node.IP
333+
}
334+
}
335+
336+
if targetIP != "" {
337+
if sshKey == "" {
338+
addCheck("SSH connectivity", true, "skipped; set SSH_KEY to verify remote connectivity")
339+
} else {
340+
user := "root"
341+
if cfg != nil && cfg.Node.USERNAME != "" {
342+
user = cfg.Node.USERNAME
343+
}
344+
client, err := sshutil.NewSSHClient(user, targetIP, 22, sshKey)
345+
if err != nil {
346+
addCheck("SSH connectivity", false, err.Error())
347+
} else {
348+
_ = client.Close()
349+
addCheck("SSH connectivity", true, fmt.Sprintf("connected as %s@%s", user, targetIP))
350+
}
351+
}
352+
} else {
353+
addCheck("SSH connectivity", true, "skipped; pass --ip or include node.ip in the config to verify a target")
354+
}
355+
356+
failed := false
357+
fmt.Println("Preflight checks:")
358+
for _, check := range checks {
359+
status := "OK"
360+
if !check.ok {
361+
status = "WARN"
362+
if check.name == "config" || check.name == "SSH connectivity" {
363+
failed = true
364+
}
365+
}
366+
fmt.Printf(" [%s] %s - %s\n", status, check.name, check.detail)
367+
}
368+
369+
if failed {
370+
os.Exit(1)
371+
}
372+
373+
fmt.Println("\n✓ Preflight completed")
374+
}
375+
376+
func commandAvailable(name string) bool {
377+
_, err := exec.LookPath(name)
378+
return err == nil
379+
}
380+
381+
func commandDetail(name string) string {
382+
path, err := exec.LookPath(name)
383+
if err != nil {
384+
return fmt.Sprintf("%s not found in PATH", name)
385+
}
386+
return fmt.Sprintf("found at %s", path)
387+
}
388+
270389
func cmdAudit(ip string) {
271390
slog.Info("Starting security audit", "target_ip", ip)
272391

gs

100644100755
-389 KB
Binary file not shown.

0 commit comments

Comments
 (0)