Files
2026-07-27 14:41:04 +02:00

9.6 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is

NodeMaster is a beego v2 REST API (Go module nodemaster) for managing a single Docker/Podman host ("the node"), a registry of remote NodeMaster nodes, and the compose-based services running on the local node — including a scheduled/on-demand backup system for both the node and individual services.

Commands

go build ./...              # build
go vet ./...                # vet
gofmt -l .                  # list files needing formatting (gofmt -w to fix)
go test ./...                # run all tests
go test ./tests/ -run TestGetNode   # run a single test
go run . scan [folder] [--json]     # CLI: scan compose folders, don't start the HTTP server
go run . info                       # CLI: print node summary, don't start the HTTP server
go run .                            # start the HTTP API (beego.Run(), port from conf/app.conf)
bee run -gendoc=true -downdoc=true  # dev server: rebuild-on-change + regenerate swagger/swagger.json on every save
bee generate routers                # regenerate routers/commentsRouter.go from @router annotations (run after adding/changing any controller method)

Swagger UI is served at /swagger when runmode = dev (see conf/app.conf). The spec files swagger/swagger.json / swagger/swagger.yml and routers/commentsRouter.go are all generated — never hand-edit them. Run bee run -gendoc=true -downdoc=true for day-to-day dev (it rebuilds and regenerates the swagger spec on every file change, and downloads the swagger UI static assets via -downdoc if swagger/index.html is missing); after adding, removing, or changing any controller method's @router annotation, also run bee generate routers to regenerate routers/commentsRouter.go (see routing note below). Two pre-existing @Success/@Param annotations reference types bee generate docs can't resolve (a package-local unexported struct nodeServices in controllers/nodes.go, and the pseudo-type object in controllers/node.go's scan endpoint) — this prints a harmless Cannot find the object: ... warning and generates a placeholder empty-object schema for those two entries; it does not affect generation success or the routes themselves.

There is no README and no lint config beyond go vet/gofmt.

Architecture

Config persistence (models/store.go): the entire application state — local node info, remote node registry, and services — is one AppConfig struct, serialized as JSON to /etc/nodemaster/nodemaster.conf (falls back to ~/.nodemaster.conf if that path isn't writable). A single package-level sync.RWMutex (mu) guards the in-memory cfg *AppConfig. Every mutating function calls persist() to write the JSON back out. Because the config can hold a restic repository password (see below), persist() writes/chmods the file 0600.

API namespaces (all under /v1, wired in routers/router.go):

  • /node — the local host: info, update history, backup config/targets/run, compose scan
  • /nodes — remote node registry, plus GET /nodes/aggregated which calls each registered node's own /v1/services over HTTP and merges the results
  • /services — compose-based services on the local node: CRUD, start/stop, backup config/targets/run

Critical routing gotcha: beego's NSInclude resolves controller routes from beego.GlobalControllerRouter, which is populated by routers/commentsRouter.go — a generated file, rebuilt by bee generate routers (with no flags: this project's ctrlDir/routersFile match bee v2's defaults, controllers and routers/commentsRouter.go). If you add, remove, or change a controller method's @router annotation, you must run bee generate routers afterwards, or the route will silently 404 (it won't error at startup — it just won't be registered). Never hand-edit commentsRouter.go; a plain rerun of bee generate routers overwrites it in place from the current @router comments. (Historically this file was hand-maintained under the name commentsRouter_controllers.go after bee generate routers seemed unreliable — the actual issue was just that the default bee output path is routers/commentsRouter.go, not commentsRouter_controllers.go; running the generator with that stale filename created a second, duplicate router file instead of updating the existing one. Regenerating at the correct default path resolved it.)

Backup execution (models/backup.go): three methods — MethodRsync, MethodRestic, MethodExternal (no rclone; it was removed in favor of restic). BackupTarget.Remote is overloaded by method: an rsync destination (user@host:/path) or a restic repository string (/mnt/backup/repo, s3:..., sftp:..., etc.); BackupTarget.Params becomes extra --key=value CLI flags for whichever tool runs. MethodExternal is a no-op (the operator manages that backup outside the API). Restic's repository password is node-global, not per-target: NodeInfo.ResticPassword, read via models.GetResticPassword(), set through PUT /v1/node's restic_password field (never echoed back — GET /v1/node only exposes restic_password_set: bool). It's passed to the restic binary via RESTIC_PASSWORD/RESTIC_REPOSITORY env vars, never argv. runTarget calls ensureResticRepo() (runs restic init, tolerates "already initialized") before every restic backup rather than tracking init state separately.

Backup targets are individually CRUD-able, not just a whole-BackupConfig replace: /v1/node/backup/targets[...] and /v1/services/:id/backup/targets[...], each with :tid sub-routes for get/put/delete/run/progress, plus a .../backup/progress aggregate view. BackupTarget.Schedule is a 5-field standard cron expression (empty = manual-only). Validated centrally in validateBackupTarget (models/backup.go) — this is the single place that knows the set of valid BackupMethod values and schedule syntax.

Scheduling (models/scheduler.go): an in-process robfig/cron/v3 scheduler. StartScheduler() is called once from main.go, only on the normal HTTP-server path (not on the scan/info CLI paths). SyncSchedule() fully tears down and rebuilds every cron entry from the current target definitions in cfg, and must be called after any mutation that can change Targets — not just the per-target CRUD endpoints, but also whole-object replace paths (UpdateNodeBackupConfig, AddService, UpdateService, DeleteService), since those can silently add/remove/alter schedules too. Any new way of mutating backup targets needs a SyncSchedule() call after persist().

Progress tracking (models/progress.go): an in-memory-only RunState map (its own mutex, deliberately kept out of cfg/persist() — a crash/restart must never leave a stale "running" flag on disk). Populated by models/backup.go's progressWriter, wired as both cmd.Stdout and cmd.Stderr: rsync via --info=progress2 (regex on the NN% token), restic via backup --json (parses message_type lines: status for percent/ETA, summary for the final snapshot id, error for failures). Parsing is best-effort — an unparsed line just leaves the last known percent/message in place rather than failing the backup.

Shared run helpers: models.RunNodeBackup(targetIDs []string) and models.RunServiceBackup(id string, targetIDs []string) are the single code path used by manual-trigger controllers, the scheduler, and the "run all targets" endpoints. Don't duplicate stop/start/backup/persist logic in a controller — route new triggers through these.

Compose support: docker compose (default), docker-compose, podman-compose, selected per-service via Service.ComposeType (models/backup.go's composeCmd). Services can be configured to stop before backup (StopOnBackup=true) and restart after; all backup runs are asynchronous (goroutine) and append a BackupExecution record to history.

Compose scan (models/scan.go, POST /v1/node/scan or nodemaster scan [folder] [--json]):

  • Scans /opt/compose, /root/compose, /home/*/compose by default, plus an optional extra folder
  • Parses compose YAML to deduce a backup folder (prefers /opt/data/containers/<name>, falls back to the common path prefix across volume mounts)
  • Probes live status via docker ps --filter label=com.docker.compose.project=<name>
  • Falls back to a text-based parser for compose files with duplicate YAML keys (which gopkg.in/yaml.v3 rejects)

Key files

  • models/types.go — every persisted struct (AppConfig, NodeInfo, RemoteNode, Service, BackupConfig, BackupTarget, BackupExecution, ScanResult, etc.)
  • models/store.go — the persistence layer (cfg, mu, persist(), config path resolution)
  • models/backup.goRunBackup/runTarget, progressWriter, validateBackupTarget, StartService/StopService/composeCmd
  • models/progress.go — in-memory RunState tracking (not persisted)
  • models/scheduler.go — cron wiring: StartScheduler/SyncSchedule
  • models/scan.goScanAll, compose YAML parsing, backup-folder deduction
  • models/node.go / models/nodes.go / models/services.go — CRUD for the local node, remote node registry, and services respectively
  • controllers/*.go — beego controllers with @router annotations (source of truth for API surface)
  • routers/commentsRouter.go — generated beego route registration, rebuilt via bee generate routers (see routing gotcha above) — never hand-edit
  • routers/router.go — namespace wiring + dev-mode CORS filter
  • main.go — entrypoint; also implements the scan/info CLI subcommands ahead of the normal beego.Run() path