ananke/testing/orchestrator/hooks_startup_scope_vault_test.go

223 lines
10 KiB
Go
Raw Permalink Normal View History

package orchestrator
import (
"context"
"errors"
"os"
"strings"
"testing"
"time"
"scm.bstein.dev/bstein/ananke/internal/cluster"
)
// readStartupProgress runs one orchestration or CLI step.
// Signature: readStartupProgress(t *testing.T, orch *cluster.Orchestrator) string.
// Why: startup helper tests need to inspect progress artifacts without reaching
// into internal package state from the top-level testing module.
func readStartupProgress(t *testing.T, orch *cluster.Orchestrator) string {
t.Helper()
payload, err := os.ReadFile(orch.TestHookStartupProgressPath())
if err != nil {
t.Fatalf("read startup progress: %v", err)
}
return string(payload)
}
// TestHookStartupScopeAndVaultHelpers runs one orchestration or CLI step.
// Signature: TestHookStartupScopeAndVaultHelpers(t *testing.T).
// Why: keeps startup-scope and startup-Vault helper branches covered from the
// split top-level testing module required by the repo hygiene contract.
func TestHookStartupScopeAndVaultHelpers(t *testing.T) {
t.Run("startup-scope-helpers", func(t *testing.T) {
nodes := []string{"titan-db", " titan-23 ", "", "titan-24"}
if got := cluster.TestHookStartupRequiredNodes(nodes, nil); len(got) != len(nodes) {
t.Fatalf("expected passthrough node list, got %v", got)
}
got := cluster.TestHookStartupRequiredNodes(nodes, []string{"titan-db", "titan-24"})
if len(got) != 2 || got[0] != "titan-db" || got[1] != "titan-24" {
t.Fatalf("unexpected filtered node list: %v", got)
}
if !cluster.TestHookContainsNode([]string{"titan-db", " titan-23 "}, " titan-23 ") {
t.Fatalf("expected trimmed node membership match")
}
if cluster.TestHookContainsNode([]string{"titan-db"}, " ") {
t.Fatalf("expected blank node probe to be ignored")
}
cfg := lifecycleConfig(t)
orch, _ := newHookOrchestrator(t, cfg, nil, nil)
if !orch.TestHookStartupNodeStrictlyRequired("titan-23") {
t.Fatalf("expected all nodes to be strict when no recovery scopes are configured")
}
cfgScoped := lifecycleConfig(t)
cfgScoped.Startup.NodeInventoryReachRequiredNodes = []string{"titan-23"}
cfgScoped.Startup.NodeSSHAuthRequiredNodes = []string{"titan-24"}
cfgScoped.Startup.FluxHealthRequiredKustomizations = []string{"flux-system/core", " flux-system/gitea "}
cfgScoped.Startup.WorkloadConvergenceRequiredNamespaces = []string{"vault", " monitoring "}
orchScoped, _ := newHookOrchestrator(t, cfgScoped, nil, nil)
if !orchScoped.TestHookStartupNodeStrictlyRequired("titan-db") {
t.Fatalf("expected control plane to remain strict")
}
if !orchScoped.TestHookStartupNodeStrictlyRequired("titan-23") {
t.Fatalf("expected inventory-scoped node to remain strict")
}
if !orchScoped.TestHookStartupNodeStrictlyRequired("titan-24") {
t.Fatalf("expected ssh-scoped node to remain strict")
}
if orchScoped.TestHookStartupNodeStrictlyRequired("titan-25") {
t.Fatalf("expected non-core worker to stop being strict")
}
flux := orchScoped.TestHookStartupRequiredFluxKustomizations()
if _, ok := flux["flux-system/core"]; !ok {
t.Fatalf("expected core flux kustomization in required set: %v", flux)
}
if _, ok := flux["flux-system/gitea"]; !ok {
t.Fatalf("expected trimmed gitea kustomization in required set: %v", flux)
}
namespaces := orchScoped.TestHookStartupRequiredWorkloadNamespaces()
if _, ok := namespaces["vault"]; !ok {
t.Fatalf("expected vault namespace in required set: %v", namespaces)
}
if _, ok := namespaces["monitoring"]; !ok {
t.Fatalf("expected trimmed monitoring namespace in required set: %v", namespaces)
}
})
t.Run("startup-vault-helpers", func(t *testing.T) {
t.Run("early-vault-unseal-paths", func(t *testing.T) {
cfgAPI := lifecycleConfig(t)
runAPI := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) {
command := name + " " + strings.Join(args, " ")
if name == "kubectl" && strings.Contains(command, "version --request-timeout=5s") {
return "", errors.New("api down")
}
return lifecycleDispatcher(&commandRecorder{})(ctx, timeout, name, args...)
}
orchAPI, _ := newHookOrchestrator(t, cfgAPI, runAPI, runAPI)
orchAPI.TestHookBeginStartupReport("startup-vault")
orchAPI.TestHookMaybeRunEarlyVaultUnseal(context.Background())
if payload := readStartupProgress(t, orchAPI); strings.Contains(payload, "vault-unseal-early") {
t.Fatalf("expected no early vault check when api is unavailable, payload=%s", payload)
}
cfgErr := lifecycleConfig(t)
runErr := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) {
command := name + " " + strings.Join(args, " ")
switch {
case name == "kubectl" && strings.Contains(command, "version --request-timeout=5s"):
return "v1.31.0", nil
case name == "kubectl" && strings.Contains(command, "-n vault get pod vault-0 -o jsonpath={.status.phase}"):
return "", errors.New("phase probe failed")
default:
return lifecycleDispatcher(&commandRecorder{})(ctx, timeout, name, args...)
}
}
orchErr, _ := newHookOrchestrator(t, cfgErr, runErr, runErr)
orchErr.TestHookBeginStartupReport("startup-vault")
orchErr.TestHookMaybeRunEarlyVaultUnseal(context.Background())
if payload := readStartupProgress(t, orchErr); !strings.Contains(payload, "deferred early vault unseal") {
t.Fatalf("expected early vault auto-heal detail, payload=%s", payload)
}
cfgDeferred := lifecycleConfig(t)
runDeferred := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) {
command := name + " " + strings.Join(args, " ")
switch {
case name == "kubectl" && strings.Contains(command, "version --request-timeout=5s"):
return "v1.31.0", nil
case name == "kubectl" && strings.Contains(command, "-n vault get pod vault-0 -o jsonpath={.status.phase}"):
return "Pending", nil
default:
return lifecycleDispatcher(&commandRecorder{})(ctx, timeout, name, args...)
}
}
orchDeferred, _ := newHookOrchestrator(t, cfgDeferred, runDeferred, runDeferred)
orchDeferred.TestHookBeginStartupReport("startup-vault")
orchDeferred.TestHookMaybeRunEarlyVaultUnseal(context.Background())
if payload := readStartupProgress(t, orchDeferred); !strings.Contains(payload, `vault-0 pod phase is \"Pending\"`) {
t.Fatalf("expected deferred early vault detail, payload=%s", payload)
}
cfgSuccess := lifecycleConfig(t)
runSuccess := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) {
command := name + " " + strings.Join(args, " ")
switch {
case name == "kubectl" && strings.Contains(command, "version --request-timeout=5s"):
return "v1.31.0", nil
case name == "kubectl" && strings.Contains(command, "-n vault get pod vault-0 -o jsonpath={.status.phase}"):
return "Running", nil
case name == "kubectl" && strings.Contains(command, "vault status -format=json"):
return `{"sealed":false,"initialized":true}`, nil
default:
return lifecycleDispatcher(&commandRecorder{})(ctx, timeout, name, args...)
}
}
orchSuccess, _ := newHookOrchestrator(t, cfgSuccess, runSuccess, runSuccess)
orchSuccess.TestHookBeginStartupReport("startup-vault")
orchSuccess.TestHookMaybeRunEarlyVaultUnseal(context.Background())
if payload := readStartupProgress(t, orchSuccess); !strings.Contains(payload, `"vault-unseal-early"`) || !strings.Contains(payload, `"passed"`) {
t.Fatalf("expected successful early vault check, payload=%s", payload)
}
})
t.Run("startup-vault-gate-paths", func(t *testing.T) {
cfgErr := lifecycleConfig(t)
runErr := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) {
command := name + " " + strings.Join(args, " ")
if name == "kubectl" && strings.Contains(command, "-n vault get pod vault-0 -o jsonpath={.status.phase}") {
return "", errors.New("phase probe failed")
}
return lifecycleDispatcher(&commandRecorder{})(ctx, timeout, name, args...)
}
orchErr, _ := newHookOrchestrator(t, cfgErr, runErr, runErr)
orchErr.TestHookBeginStartupReport("startup-vault")
if err := orchErr.TestHookRunStartupVaultUnsealGate(context.Background()); err == nil || !strings.Contains(err.Error(), "phase probe failed") {
t.Fatalf("expected startup vault gate error, got %v", err)
}
cfgDeferred := lifecycleConfig(t)
runDeferred := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) {
command := name + " " + strings.Join(args, " ")
if name == "kubectl" && strings.Contains(command, "-n vault get pod vault-0 -o jsonpath={.status.phase}") {
return "Pending", nil
}
return lifecycleDispatcher(&commandRecorder{})(ctx, timeout, name, args...)
}
orchDeferred, _ := newHookOrchestrator(t, cfgDeferred, runDeferred, runDeferred)
orchDeferred.TestHookBeginStartupReport("startup-vault")
if err := orchDeferred.TestHookRunStartupVaultUnsealGate(context.Background()); err != nil {
t.Fatalf("expected deferred startup vault gate to succeed, got %v", err)
}
if payload := readStartupProgress(t, orchDeferred); !strings.Contains(payload, `vault-0 pod phase is \"Pending\"`) {
t.Fatalf("expected deferred startup vault detail, payload=%s", payload)
}
cfgSuccess := lifecycleConfig(t)
runSuccess := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) {
command := name + " " + strings.Join(args, " ")
switch {
case name == "kubectl" && strings.Contains(command, "-n vault get pod vault-0 -o jsonpath={.status.phase}"):
return "Running", nil
case name == "kubectl" && strings.Contains(command, "vault status -format=json"):
return `{"sealed":false,"initialized":true}`, nil
default:
return lifecycleDispatcher(&commandRecorder{})(ctx, timeout, name, args...)
}
}
orchSuccess, _ := newHookOrchestrator(t, cfgSuccess, runSuccess, runSuccess)
orchSuccess.TestHookBeginStartupReport("startup-vault")
if err := orchSuccess.TestHookRunStartupVaultUnsealGate(context.Background()); err != nil {
t.Fatalf("expected successful startup vault gate, got %v", err)
}
if payload := readStartupProgress(t, orchSuccess); !strings.Contains(payload, `"vault is unsealed"`) {
t.Fatalf("expected successful startup vault detail, payload=%s", payload)
}
})
})
}