docs: update design with configurable subjects and improved module
- Add configurable NATS subject patterns with template variables (<hostname>, <tier>, <role>) 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 <noreply@anthropic.com>
This commit is contained in:
@@ -40,7 +40,7 @@ Subjects follow `deploy.<tier>.<target>`:
|
||||
### 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
|
||||
|
||||
241
design.md
241
design.md
@@ -62,7 +62,20 @@ homelab-deploy listener \
|
||||
--nkey-file /path/to/listener.nkey \
|
||||
--flake-url <git+https://...> \
|
||||
[--role <role>] \
|
||||
[--timeout 600]
|
||||
[--timeout 600] \
|
||||
[--deploy-subject <subject>]... \
|
||||
[--discover-subject <subject>]
|
||||
|
||||
# Subject flags can be repeated and use template variables:
|
||||
homelab-deploy listener \
|
||||
--hostname ns1 \
|
||||
--tier prod \
|
||||
--role dns \
|
||||
--deploy-subject "deploy.<tier>.<hostname>" \
|
||||
--deploy-subject "deploy.<tier>.all" \
|
||||
--deploy-subject "deploy.<tier>.role.<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 <hostname> \
|
||||
# Deploy to a specific subject
|
||||
homelab-deploy deploy <subject> \
|
||||
--nats-url nats://server:4222 \
|
||||
--nkey-file /path/to/deployer.nkey \
|
||||
[--branch <branch>] \
|
||||
[--action <switch|boot|test|dry-activate>]
|
||||
|
||||
homelab-deploy deploy \
|
||||
--tier <test|prod> \
|
||||
--all \
|
||||
--nats-url nats://server:4222 \
|
||||
--nkey-file /path/to/deployer.nkey \
|
||||
[--branch <branch>] \
|
||||
[--action <switch|boot|test|dry-activate>]
|
||||
# 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 <test|prod> \
|
||||
--role <role> \
|
||||
--nats-url nats://server:4222 \
|
||||
--nkey-file /path/to/deployer.nkey \
|
||||
[--branch <branch>] \
|
||||
[--action <switch|boot|test|dry-activate>]
|
||||
# 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 `<subject>` argument doesn't look like a NATS subject (no dots), the CLI checks for an alias.
|
||||
|
||||
**Environment variable format:** `HOMELAB_DEPLOY_ALIAS_<NAME>=<subject>`
|
||||
|
||||
```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.<tier>.<target>`:
|
||||
Subjects follow the pattern `deploy.<tier>.<target>` by default, but are fully configurable:
|
||||
|
||||
| Subject Pattern | Description |
|
||||
|-----------------|-------------|
|
||||
| `deploy.<tier>.<hostname>` | Deploy to specific host (e.g., `deploy.prod.ns1`) |
|
||||
| `deploy.<tier>.all` | Deploy to all hosts in tier (e.g., `deploy.test.all`) |
|
||||
| `deploy.<tier>.role.<role>` | Deploy to hosts with role in tier (e.g., `deploy.prod.role.dns`) |
|
||||
| `deploy.responses.<request-id>` | Response subject for request/reply pattern |
|
||||
| `deploy.responses.<uuid>` | 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:
|
||||
- `<hostname>` - The listener's hostname
|
||||
- `<tier>` - The listener's tier (test/prod)
|
||||
- `<role>` - The listener's role (if configured)
|
||||
|
||||
This allows prefixing subjects for multi-tenant setups (e.g., `homelab.deploy.<tier>.<hostname>`).
|
||||
|
||||
## 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.<tier>.<hostname>` - Direct messages to this host
|
||||
- `deploy.<tier>.all` - Broadcast to all hosts in tier
|
||||
- `deploy.<tier>.role.<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.<tier>.<hostname>"
|
||||
- "deploy.<tier>.all"
|
||||
- "deploy.<tier>.role.<role>"
|
||||
|
||||
discover_subject: "deploy.discover"
|
||||
```
|
||||
|
||||
Template variables:
|
||||
- `<hostname>` - Replaced with the configured hostname
|
||||
- `<tier>` - Replaced with the configured tier
|
||||
- `<role>` - 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.<tier>.<hostname>"
|
||||
- "homelab.deploy.<tier>.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.<uuid>`
|
||||
3. CLI publishes deploy request to target subject with `reply_to: "deploy.responses.<uuid>"`
|
||||
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.<tier>.<hostname>"
|
||||
"deploy.<tier>.all"
|
||||
"deploy.<tier>.role.<role>"
|
||||
];
|
||||
description = ''
|
||||
List of NATS subjects to subscribe to for deployment requests.
|
||||
Template variables: <hostname>, <tier>, <role>
|
||||
'';
|
||||
};
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user