feat: implement NATS-based NixOS deployment system
Implement the complete homelab-deploy system with three operational modes: - Listener mode: Runs on NixOS hosts as a systemd service, subscribes to NATS subjects with configurable templates, executes nixos-rebuild on deployment requests with concurrency control - MCP mode: MCP server exposing deploy, deploy_admin, and list_hosts tools for AI assistants with tiered access control - CLI mode: Manual deployment commands with subject alias support via environment variables Key components: - internal/messages: Request/response types with validation - internal/nats: Client wrapper with NKey authentication - internal/deploy: Executor with timeout and lock for concurrency - internal/listener: Subject template expansion and request handling - internal/cli: Deploy logic with alias resolution - internal/mcp: MCP server with mcp-go integration - nixos/module.nix: NixOS module with hardened systemd service Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
109
internal/cli/deploy_test.go
Normal file
109
internal/cli/deploy_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.t-juice.club/torjus/homelab-deploy/internal/messages"
|
||||
)
|
||||
|
||||
func TestDeployResult_AllSucceeded(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
responses []*messages.DeployResponse
|
||||
errors []error
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "all completed",
|
||||
responses: []*messages.DeployResponse{
|
||||
{Hostname: "host1", Status: messages.StatusCompleted},
|
||||
{Hostname: "host2", Status: messages.StatusCompleted},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "one failed",
|
||||
responses: []*messages.DeployResponse{
|
||||
{Hostname: "host1", Status: messages.StatusCompleted},
|
||||
{Hostname: "host2", Status: messages.StatusFailed},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "one rejected",
|
||||
responses: []*messages.DeployResponse{
|
||||
{Hostname: "host1", Status: messages.StatusRejected},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "no responses",
|
||||
responses: []*messages.DeployResponse{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "has errors",
|
||||
responses: []*messages.DeployResponse{
|
||||
{Hostname: "host1", Status: messages.StatusCompleted},
|
||||
},
|
||||
errors: []error{nil}, // placeholder error
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := &DeployResult{
|
||||
Responses: tc.responses,
|
||||
Errors: tc.errors,
|
||||
}
|
||||
got := r.AllSucceeded()
|
||||
if got != tc.want {
|
||||
t.Errorf("AllSucceeded() = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployResult_HostCount(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
responses []*messages.DeployResponse
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "no responses",
|
||||
responses: []*messages.DeployResponse{},
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "unique hosts",
|
||||
responses: []*messages.DeployResponse{
|
||||
{Hostname: "host1"},
|
||||
{Hostname: "host2"},
|
||||
{Hostname: "host3"},
|
||||
},
|
||||
want: 3,
|
||||
},
|
||||
{
|
||||
name: "duplicate hosts",
|
||||
responses: []*messages.DeployResponse{
|
||||
{Hostname: "host1", Status: messages.StatusStarted},
|
||||
{Hostname: "host1", Status: messages.StatusCompleted},
|
||||
{Hostname: "host2", Status: messages.StatusStarted},
|
||||
{Hostname: "host2", Status: messages.StatusCompleted},
|
||||
},
|
||||
want: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := &DeployResult{Responses: tc.responses}
|
||||
got := r.HostCount()
|
||||
if got != tc.want {
|
||||
t.Errorf("HostCount() = %d, want %d", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user