190 lines
6.4 KiB
Go
190 lines
6.4 KiB
Go
package cluster
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"scm.bstein.dev/bstein/ananke/internal/config"
|
|
"scm.bstein.dev/bstein/ananke/internal/execx"
|
|
)
|
|
|
|
// TestValidateNodeInventoryPassesForStrictMappings runs one orchestration or CLI step.
|
|
// Signature: TestValidateNodeInventoryPassesForStrictMappings(t *testing.T).
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
|
func TestValidateNodeInventoryPassesForStrictMappings(t *testing.T) {
|
|
orch := &Orchestrator{
|
|
cfg: config.Config{
|
|
SSHUser: "atlas",
|
|
SSHPort: 2277,
|
|
SSHNodeHosts: map[string]string{
|
|
"titan-0a": "192.168.22.11",
|
|
"titan-0b": "192.168.22.12",
|
|
"titan-0c": "192.168.22.13",
|
|
"titan-22": "192.168.22.22",
|
|
},
|
|
SSHManagedNodes: []string{"titan-0a", "titan-0b", "titan-0c", "titan-22"},
|
|
ControlPlanes: []string{"titan-0a", "titan-0b", "titan-0c"},
|
|
Workers: []string{"titan-22"},
|
|
},
|
|
log: log.New(os.Stdout, "", 0),
|
|
}
|
|
if err := orch.validateNodeInventory(); err != nil {
|
|
t.Fatalf("expected inventory to pass, got error: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestValidateNodeInventoryFailsWhenNodeMappingMissing runs one orchestration or CLI step.
|
|
// Signature: TestValidateNodeInventoryFailsWhenNodeMappingMissing(t *testing.T).
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
|
func TestValidateNodeInventoryFailsWhenNodeMappingMissing(t *testing.T) {
|
|
orch := &Orchestrator{
|
|
cfg: config.Config{
|
|
SSHUser: "atlas",
|
|
SSHPort: 2277,
|
|
SSHNodeHosts: map[string]string{"titan-0a": "192.168.22.11"},
|
|
SSHManagedNodes: []string{"titan-0a", "titan-0b"},
|
|
ControlPlanes: []string{"titan-0a"},
|
|
Workers: []string{"titan-0b"},
|
|
},
|
|
log: log.New(os.Stdout, "", 0),
|
|
}
|
|
err := orch.validateNodeInventory()
|
|
if err == nil {
|
|
t.Fatalf("expected inventory error for missing mapping")
|
|
}
|
|
if !strings.Contains(err.Error(), "missing ssh_node_hosts entry") {
|
|
t.Fatalf("expected missing-mapping detail, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestValidateNodeInventoryFailsWhenWorkerNotManaged runs one orchestration or CLI step.
|
|
// Signature: TestValidateNodeInventoryFailsWhenWorkerNotManaged(t *testing.T).
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
|
func TestValidateNodeInventoryFailsWhenWorkerNotManaged(t *testing.T) {
|
|
orch := &Orchestrator{
|
|
cfg: config.Config{
|
|
SSHUser: "atlas",
|
|
SSHPort: 2277,
|
|
SSHNodeHosts: map[string]string{
|
|
"titan-0a": "192.168.22.11",
|
|
"titan-22": "192.168.22.22",
|
|
},
|
|
SSHManagedNodes: []string{"titan-0a"},
|
|
ControlPlanes: []string{"titan-0a"},
|
|
Workers: []string{"titan-22"},
|
|
},
|
|
log: log.New(os.Stdout, "", 0),
|
|
}
|
|
err := orch.validateNodeInventory()
|
|
if err == nil {
|
|
t.Fatalf("expected inventory error for unmanaged worker")
|
|
}
|
|
if !strings.Contains(err.Error(), "missing from ssh_managed_nodes") {
|
|
t.Fatalf("expected unmanaged-worker detail, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestNormalizeShutdownModeRejectsPoweroff runs one orchestration or CLI step.
|
|
// Signature: TestNormalizeShutdownModeRejectsPoweroff(t *testing.T).
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
|
func TestNormalizeShutdownModeRejectsPoweroff(t *testing.T) {
|
|
if _, err := normalizeShutdownMode("poweroff"); err == nil {
|
|
t.Fatalf("expected poweroff mode to be rejected")
|
|
}
|
|
if mode, err := normalizeShutdownMode("config"); err != nil || mode != "cluster-only" {
|
|
t.Fatalf("expected config mode to normalize to cluster-only, mode=%q err=%v", mode, err)
|
|
}
|
|
}
|
|
|
|
// TestWaitForNodeInventoryReachabilityPasses runs one orchestration or CLI step.
|
|
// Signature: TestWaitForNodeInventoryReachabilityPasses(t *testing.T).
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
|
func TestWaitForNodeInventoryReachabilityPasses(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
sshPath := filepath.Join(tmp, "ssh")
|
|
sshScript := `#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
target=""
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
*@*) target="$arg" ;;
|
|
esac
|
|
done
|
|
if [[ "$target" == *"titan-0a"* ]] || [[ "$target" == *"titan-0b"* ]]; then
|
|
echo "__ANANKE_NODE_REACHABLE__"
|
|
exit 0
|
|
fi
|
|
echo "no route to host" >&2
|
|
exit 255
|
|
`
|
|
if err := os.WriteFile(sshPath, []byte(sshScript), 0o755); err != nil {
|
|
t.Fatalf("write fake ssh: %v", err)
|
|
}
|
|
t.Setenv("PATH", tmp+":"+os.Getenv("PATH"))
|
|
|
|
orch := &Orchestrator{
|
|
cfg: config.Config{
|
|
SSHUser: "atlas",
|
|
SSHPort: 2277,
|
|
ControlPlanes: []string{"titan-0a"},
|
|
Workers: []string{"titan-0b"},
|
|
SSHManagedNodes: []string{"titan-0a", "titan-0b"},
|
|
Startup: config.Startup{
|
|
RequireNodeInventoryReach: true,
|
|
NodeInventoryReachWaitSeconds: 2,
|
|
NodeInventoryReachPollSeconds: 1,
|
|
IgnoreUnavailableNodes: []string{},
|
|
},
|
|
},
|
|
runner: &execx.Runner{DryRun: false},
|
|
log: log.New(os.Stdout, "", 0),
|
|
}
|
|
if err := orch.waitForNodeInventoryReachability(context.Background()); err != nil {
|
|
t.Fatalf("expected node inventory reachability to pass, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestWaitForNodeInventoryReachabilityFails runs one orchestration or CLI step.
|
|
// Signature: TestWaitForNodeInventoryReachabilityFails(t *testing.T).
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
|
func TestWaitForNodeInventoryReachabilityFails(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
sshPath := filepath.Join(tmp, "ssh")
|
|
sshScript := `#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
echo "no route to host" >&2
|
|
exit 255
|
|
`
|
|
if err := os.WriteFile(sshPath, []byte(sshScript), 0o755); err != nil {
|
|
t.Fatalf("write fake ssh: %v", err)
|
|
}
|
|
t.Setenv("PATH", tmp+":"+os.Getenv("PATH"))
|
|
|
|
orch := &Orchestrator{
|
|
cfg: config.Config{
|
|
SSHUser: "atlas",
|
|
SSHPort: 2277,
|
|
ControlPlanes: []string{"titan-0a"},
|
|
SSHManagedNodes: []string{"titan-0a"},
|
|
Startup: config.Startup{
|
|
RequireNodeInventoryReach: true,
|
|
NodeInventoryReachWaitSeconds: 1,
|
|
NodeInventoryReachPollSeconds: 1,
|
|
},
|
|
},
|
|
runner: &execx.Runner{DryRun: false},
|
|
log: log.New(os.Stdout, "", 0),
|
|
}
|
|
err := orch.waitForNodeInventoryReachability(context.Background())
|
|
if err == nil {
|
|
t.Fatalf("expected node inventory reachability to fail")
|
|
}
|
|
if !strings.Contains(err.Error(), "did not pass within") {
|
|
t.Fatalf("expected timeout detail in error, got: %v", err)
|
|
}
|
|
}
|