From 1f23a6ddc9763dabb600f092fe0f01983edf82f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torjus=20H=C3=A5kestad?= Date: Sat, 7 Feb 2026 03:52:01 +0100 Subject: [PATCH] docs: update design with configurable subjects and improved module - Add configurable NATS subject patterns with template variables (, , ) for multi-tenant setups - Add deploy.discover subject for host discovery - Simplify CLI to use direct subjects with optional aliases via HOMELAB_DEPLOY_ALIAS_* environment variables - Clarify request/reply flow with UUID-based response subjects - Expand NixOS module with hardening options, package option, and configurable deploy/discover subjects - Switch CLI framework from cobra to urfave/cli/v3 Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 2 +- design.md | 241 +++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 196 insertions(+), 47 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b796fd9..8522953 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,7 +40,7 @@ Subjects follow `deploy..`: ### Planned Package Structure ``` -cmd/homelab-deploy/main.go # CLI entrypoint with cobra subcommands +cmd/homelab-deploy/main.go # CLI entrypoint with urfave/cli subcommands internal/listener/ # Listener mode (NATS subscription, nixos-rebuild execution) internal/mcp/ # MCP server mode internal/nats/ # NATS client wrapper diff --git a/design.md b/design.md index 33e0dbb..d11729f 100644 --- a/design.md +++ b/design.md @@ -62,7 +62,20 @@ homelab-deploy listener \ --nkey-file /path/to/listener.nkey \ --flake-url \ [--role ] \ - [--timeout 600] + [--timeout 600] \ + [--deploy-subject ]... \ + [--discover-subject ] + +# Subject flags can be repeated and use template variables: +homelab-deploy listener \ + --hostname ns1 \ + --tier prod \ + --role dns \ + --deploy-subject "deploy.." \ + --deploy-subject "deploy..all" \ + --deploy-subject "deploy..role." \ + --discover-subject "deploy.discover" \ + ... # MCP server mode (for AI assistants) homelab-deploy mcp \ @@ -71,62 +84,113 @@ homelab-deploy mcp \ [--enable-admin --admin-nkey-file /path/to/admin.nkey] # CLI commands for manual use -homelab-deploy deploy \ +# Deploy to a specific subject +homelab-deploy deploy \ --nats-url nats://server:4222 \ --nkey-file /path/to/deployer.nkey \ [--branch ] \ [--action ] -homelab-deploy deploy \ - --tier \ - --all \ - --nats-url nats://server:4222 \ - --nkey-file /path/to/deployer.nkey \ - [--branch ] \ - [--action ] +# Examples: +homelab-deploy deploy deploy.prod.ns1 # Deploy to specific host +homelab-deploy deploy deploy.test.all # Deploy to all test hosts +homelab-deploy deploy deploy.prod.role.dns # Deploy to all prod DNS hosts -homelab-deploy deploy \ - --tier \ - --role \ - --nats-url nats://server:4222 \ - --nkey-file /path/to/deployer.nkey \ - [--branch ] \ - [--action ] +# Using aliases (configured via environment variables) +homelab-deploy deploy test # Expands to configured subject +homelab-deploy deploy prod-dns # Expands to configured subject ``` +### CLI Subject Aliases + +The CLI supports subject aliases via environment variables. If the `` argument doesn't look like a NATS subject (no dots), the CLI checks for an alias. + +**Environment variable format:** `HOMELAB_DEPLOY_ALIAS_=` + +```bash +export HOMELAB_DEPLOY_ALIAS_TEST="deploy.test.all" +export HOMELAB_DEPLOY_ALIAS_PROD="deploy.prod.all" +export HOMELAB_DEPLOY_ALIAS_PROD_DNS="deploy.prod.role.dns" + +# Now these work: +homelab-deploy deploy test # -> deploy.test.all +homelab-deploy deploy prod # -> deploy.prod.all +homelab-deploy deploy prod-dns # -> deploy.prod.role.dns +``` + +Alias names are case-insensitive and hyphens are converted to underscores when looking up the environment variable. + ## NATS Subject Structure -Subjects follow the pattern `deploy..`: +Subjects follow the pattern `deploy..` by default, but are fully configurable: | Subject Pattern | Description | |-----------------|-------------| | `deploy..` | Deploy to specific host (e.g., `deploy.prod.ns1`) | | `deploy..all` | Deploy to all hosts in tier (e.g., `deploy.test.all`) | | `deploy..role.` | Deploy to hosts with role in tier (e.g., `deploy.prod.role.dns`) | -| `deploy.responses.` | Response subject for request/reply pattern | +| `deploy.responses.` | Response subject for request/reply (UUID generated by CLI) | +| `deploy.discover` | Host discovery requests | + +### Subject Customization + +Listeners can configure custom subject patterns using template variables: +- `` - The listener's hostname +- `` - The listener's tier (test/prod) +- `` - The listener's role (if configured) + +This allows prefixing subjects for multi-tenant setups (e.g., `homelab.deploy..`). ## Listener Mode ### Responsibilities 1. Connect to NATS using NKey authentication -2. Subscribe to subjects based on hostname, tier, and role -3. Validate incoming deployment requests -4. Execute `nixos-rebuild` with the specified parameters -5. Report status back via NATS reply subject +2. Subscribe to configured deploy subjects (with template expansion) +3. Subscribe to discovery subject and respond with host metadata +4. Validate incoming deployment requests +5. Execute `nixos-rebuild` with the specified parameters +6. Report status back via NATS reply subject ### Subject Subscriptions -A listener subscribes to multiple subjects based on its configuration: +Listeners subscribe to a configurable list of subjects. The configuration uses template variables that are expanded at runtime: -- `deploy..` - Direct messages to this host -- `deploy..all` - Broadcast to all hosts in tier -- `deploy..role.` - Broadcast to hosts with matching role (only if role is configured) +```yaml +listener: + hostname: ns1 + tier: prod + role: dns -**Example:** A host with `hostname=ns1, tier=prod, role=dns` subscribes to: + deploy_subjects: + - "deploy.." + - "deploy..all" + - "deploy..role." + + discover_subject: "deploy.discover" +``` + +Template variables: +- `` - Replaced with the configured hostname +- `` - Replaced with the configured tier +- `` - Replaced with the configured role (subject skipped if role is null) + +**Example:** With the above configuration, the listener subscribes to: - `deploy.prod.ns1` - `deploy.prod.all` - `deploy.prod.role.dns` +- `deploy.discover` + +**Prefixed example:** For multi-tenant setups: +```yaml +listener: + hostname: ns1 + tier: prod + deploy_subjects: + - "homelab.deploy.." + - "homelab.deploy..all" + discover_subject: "homelab.deploy.discover" +``` ### Message Formats @@ -171,18 +235,21 @@ A listener subscribes to multiple subjects based on its configuration: ### Request/Reply Flow -1. Deployer sends request with unique `reply_to` subject -2. Deployer subscribes to the `reply_to` subject before sending -3. Listener validates request: +1. CLI generates a UUID for the request (e.g., `550e8400-e29b-41d4-a716-446655440000`) +2. CLI subscribes to `deploy.responses.` +3. CLI publishes deploy request to target subject with `reply_to: "deploy.responses."` +4. Listener validates request: - Checks revision exists using `git ls-remote` - Checks no other deployment is running -4. Listener sends immediate response: +5. Listener publishes response to the `reply_to` subject: - `{"status": "rejected", ...}` if validation fails, or - `{"status": "started", ...}` if deployment begins -5. If started, listener executes nixos-rebuild -6. Listener sends final response: +6. If started, listener executes nixos-rebuild +7. Listener publishes final response to the same `reply_to` subject: - `{"status": "completed", ...}` on success, or - `{"status": "failed", ...}` on failure +8. CLI receives responses and displays progress/results +9. CLI unsubscribes after receiving final status or timeout ### Deployment Execution @@ -354,18 +421,34 @@ The `list_hosts` tool needs to know available hosts. Options: 1. **Static configuration**: Read from a config file or environment variable 2. **NATS request**: Publish to a discovery subject and collect responses from listeners -Recommend option 2: Listeners respond to `deploy.discover` with their metadata: +Recommend option 2: Listeners subscribe to their configured `discover_subject` and respond with metadata. + +**Discovery request:** +```json +{ + "reply_to": "deploy.responses.discover-abc123" +} +``` + +**Discovery response:** ```json { "hostname": "ns1", "tier": "prod", - "role": "dns" + "role": "dns", + "deploy_subjects": [ + "deploy.prod.ns1", + "deploy.prod.all", + "deploy.prod.role.dns" + ] } ``` +The response includes the expanded `deploy_subjects` so clients know exactly which subjects reach this host. + ## NixOS Module -The NixOS module configures the listener as a systemd service. +The NixOS module configures the listener as a systemd service with appropriate hardening. ### Module Options @@ -374,9 +457,12 @@ The NixOS module configures the listener as a systemd service. options.services.homelab-deploy.listener = { enable = lib.mkEnableOption "homelab-deploy listener service"; + package = lib.mkPackageOption pkgs "homelab-deploy" { }; + hostname = lib.mkOption { type = lib.types.str; - description = "Hostname for this listener (used for NATS subject)"; + default = config.networking.hostName; + description = "Hostname for this listener (used in subject templates)"; }; tier = lib.mkOption { @@ -393,16 +479,19 @@ The NixOS module configures the listener as a systemd service. natsUrl = lib.mkOption { type = lib.types.str; description = "NATS server URL"; + example = "nats://nats.example.com:4222"; }; nkeyFile = lib.mkOption { type = lib.types.path; description = "Path to NKey seed file for NATS authentication"; + example = "/run/secrets/homelab-deploy-nkey"; }; flakeUrl = lib.mkOption { type = lib.types.str; description = "Git flake URL for nixos-rebuild"; + example = "git+https://git.example.com/user/nixos-configs.git"; }; timeout = lib.mkOption { @@ -410,19 +499,79 @@ The NixOS module configures the listener as a systemd service. default = 600; description = "Deployment timeout in seconds"; }; + + deploySubjects = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ + "deploy.." + "deploy..all" + "deploy..role." + ]; + description = '' + List of NATS subjects to subscribe to for deployment requests. + Template variables: , , + ''; + }; + + discoverSubject = lib.mkOption { + type = lib.types.str; + default = "deploy.discover"; + description = "NATS subject for host discovery requests"; + }; + + environment = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = "Additional environment variables for the service"; + example = { GIT_SSH_COMMAND = "ssh -i /run/secrets/deploy-key"; }; + }; }; } ``` ### Systemd Service -The module should create a systemd service with: -- `Type=simple` -- `Restart=always` -- `RestartSec=10` -- Run as root (required for nixos-rebuild) -- Proper ordering (after network-online.target) -- Resource limits if desired +The module creates a hardened systemd service: + +```nix +systemd.services.homelab-deploy-listener = { + description = "homelab-deploy listener"; + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + + environment = cfg.environment; + + serviceConfig = { + Type = "simple"; + ExecStart = "${cfg.package}/bin/homelab-deploy listener ..."; + Restart = "always"; + RestartSec = 10; + + # Hardening (compatible with nixos-rebuild requirements) + NoNewPrivileges = false; # nixos-rebuild may need to spawn privileged processes + ProtectSystem = "false"; # nixos-rebuild modifies /nix/store and /run + ProtectHome = "read-only"; + PrivateTmp = true; + PrivateDevices = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; + RestrictNamespaces = false; # nix build uses namespaces + RestrictSUIDSGID = true; + LockPersonality = true; + MemoryDenyWriteExecute = false; # nix may need this + SystemCallArchitectures = "native"; + }; +}; +``` + +**Note:** Some hardening options are relaxed because `nixos-rebuild` requires: +- Write access to `/nix/store` for building +- Ability to activate system configurations +- Network access for fetching from git/cache +- Namespace support for nix sandbox builds ## NATS Authentication @@ -469,9 +618,9 @@ The flake.nix should provide: ### Go Dependencies Recommended libraries: +- `github.com/urfave/cli/v3` - CLI framework - `github.com/nats-io/nats.go` - NATS client -- `github.com/spf13/cobra` - CLI framework -- `github.com/mark3labs/mcp-go` - MCP server implementation (or similar) +- `github.com/mark3labs/mcp-go` - MCP server implementation - Standard library for JSON, logging, process execution ### Error Handling