ananke/testing/orchestrator/hooks_pure_helpers_test.go

212 lines
9.0 KiB
Go
Raw Normal View History

package orchestrator
import (
"io"
"log"
"strings"
"testing"
"time"
"scm.bstein.dev/bstein/ananke/internal/cluster"
"scm.bstein.dev/bstein/ananke/internal/execx"
"scm.bstein.dev/bstein/ananke/internal/state"
)
// TestHookPureHelperCoverage runs one orchestration or CLI step.
// Signature: TestHookPureHelperCoverage(t *testing.T).
// Why: covers exported helper wrappers so parser/normalizer utility paths stay stable and test-visible.
func TestHookPureHelperCoverage(t *testing.T) {
if !cluster.TestHookProbeStatusAccepted(200) {
t.Fatalf("expected 200 accepted")
}
if !cluster.TestHookProbeStatusAccepted(401) {
t.Fatalf("expected 401 accepted for auth-fronted probes")
}
if cluster.TestHookProbeStatusAccepted(500) {
t.Fatalf("expected 500 rejected")
}
if !cluster.TestHookChecklistContains(`{"database":"ok"}`, `"database":"ok"`) {
t.Fatalf("expected exact marker match")
}
if !cluster.TestHookChecklistContains("{\n \"database\": \"ok\"\n}", `"database":"ok"`) {
t.Fatalf("expected compact whitespace-insensitive marker match")
}
if cluster.TestHookChecklistContains(`{"status":"bad"}`, `"database":"ok"`) {
t.Fatalf("unexpected marker match")
}
if !cluster.TestHookIsLikelyHostname("metrics.bstein.dev") {
t.Fatalf("expected hostname heuristic true")
}
if cluster.TestHookIsLikelyHostname("not a host/path") {
t.Fatalf("expected hostname heuristic false")
}
if got := cluster.TestHookHostFromURL("https://metrics.bstein.dev/api/health"); got != "metrics.bstein.dev" {
t.Fatalf("unexpected parsed host: %q", got)
}
now := time.Now().UTC()
if age := cluster.TestHookIntentAge(state.Intent{UpdatedAt: now.Add(-2 * time.Minute)}); age <= 0 {
t.Fatalf("expected positive intent age, got %s", age)
}
if !cluster.TestHookIntentFresh(state.Intent{UpdatedAt: now.Add(-30 * time.Second)}, 2*time.Minute) {
t.Fatalf("expected fresh intent")
}
if cluster.TestHookIntentFresh(state.Intent{UpdatedAt: now.Add(-5 * time.Minute)}, time.Minute) {
t.Fatalf("expected stale intent")
}
mode, err := cluster.TestHookNormalizeShutdownMode("cluster-only")
if err != nil || mode != "cluster-only" {
t.Fatalf("normalize shutdown mode failed: mode=%q err=%v", mode, err)
}
if _, err := cluster.TestHookNormalizeShutdownMode("poweroff"); err == nil {
t.Fatalf("expected poweroff shutdown mode rejection")
}
if _, err := cluster.TestHookNormalizeShutdownMode("invalid-mode"); err == nil {
t.Fatalf("expected invalid shutdown mode rejection")
}
if got := cluster.TestHookParseFluxKustomizationTimeout("45s"); got != 45*time.Second {
t.Fatalf("unexpected flux timeout parse: %s", got)
}
if got := cluster.TestHookParseFluxKustomizationTimeout("not-a-duration"); got != 0 {
t.Fatalf("expected zero timeout for invalid duration, got %s", got)
}
if !cluster.TestHookLooksLikeImmutableJobError("Job update failed: field is immutable") {
t.Fatalf("expected immutable job detector match")
}
if cluster.TestHookLooksLikeImmutableJobError("all good") {
t.Fatalf("unexpected immutable job detector match")
}
if !cluster.TestHookJobLooksFluxManaged("flux-system", "reconcile-services", map[string]string{"kustomize.toolkit.fluxcd.io/name": "services"}, nil) {
t.Fatalf("expected flux-managed job by label")
}
if cluster.TestHookJobLooksFluxManaged("default", "user-job", nil, []string{"CronJob"}) {
t.Fatalf("expected cronjob-owned job not treated as flux-managed")
}
if cluster.TestHookJobFailed(0, 1, []string{"Failed"}, []string{"True"}) {
t.Fatalf("expected succeeded job not failed")
}
if !cluster.TestHookJobFailed(1, 0, []string{"Failed"}, []string{"True"}) {
t.Fatalf("expected failed job detection")
}
if !cluster.TestHookIsTimeSynced("yes") || !cluster.TestHookIsTimeSynced("1") || cluster.TestHookIsTimeSynced("no") {
t.Fatalf("unexpected time sync parser behavior")
}
if got := cluster.TestHookSanitizeReportFileName("startup drill #1"); got == "" {
t.Fatalf("expected sanitized report filename")
}
snapshot := cluster.TestHookParseSnapshotPathFromEtcdSnapshotList("Name Size Created Location\npre-shutdown 4.2M now \"file:///var/lib/rancher/k3s/server/db/snapshots/pre-shutdown\"\n")
if snapshot == "" {
t.Fatalf("expected snapshot parser to extract path")
}
snapshotDirect := cluster.TestHookParseSnapshotPathFromEtcdSnapshotList("\n \npre-shutdown 4.2M now /var/lib/rancher/k3s/server/db/snapshots/direct-path,\n")
if snapshotDirect == "" {
t.Fatalf("expected direct-path snapshot parser extraction")
}
if empty := cluster.TestHookParseSnapshotPathFromEtcdSnapshotList("no rows"); empty != "" {
t.Fatalf("expected empty snapshot parse for non-matching input, got %q", empty)
}
cfg := lifecycleConfig(t)
orch := cluster.New(cfg, &execx.Runner{DryRun: true}, state.New(cfg.State.RunHistoryPath), log.New(io.Discard, "", 0))
peers := orch.TestHookCoordinationPeers()
if len(peers) != 0 {
t.Fatalf("expected no coordination peers in baseline fixture, got %v", peers)
}
_ = orch.TestHookEstimatedShutdownSeconds()
_ = orch.TestHookEstimatedEmergencyShutdownSeconds()
}
// TestHookWorkloadHelperCoverage runs one orchestration or CLI step.
// Signature: TestHookWorkloadHelperCoverage(t *testing.T).
// Why: covers workload helper wrappers so ignored-node, readiness, and failure summarization paths remain deterministic.
func TestHookWorkloadHelperCoverage(t *testing.T) {
if desired, ready, ok := cluster.TestHookDesiredReady("deployment", true, 3, 2, 0, 0); !ok || desired != 3 || ready != 2 {
t.Fatalf("unexpected desiredReady deployment result: desired=%d ready=%d ok=%v", desired, ready, ok)
}
if desired, ready, ok := cluster.TestHookDesiredReady("daemonset", false, 0, 0, 2, 1); !ok || desired != 2 || ready != 1 {
t.Fatalf("unexpected desiredReady daemonset result: desired=%d ready=%d ok=%v", desired, ready, ok)
}
if _, _, ok := cluster.TestHookDesiredReady("job", false, 0, 0, 0, 0); ok {
t.Fatalf("expected unsupported kind to return ok=false")
}
if !cluster.TestHookPodControllerOwned([]string{"ReplicaSet"}) {
t.Fatalf("expected controller-owned pod")
}
if cluster.TestHookPodControllerOwned([]string{"CronJob"}) {
t.Fatalf("expected non-controller pod for this helper")
}
reason := cluster.TestHookStuckContainerReason(
[]string{"ImagePullBackOff"},
[]string{},
[]string{"ImagePullBackOff", "ErrImagePull"},
)
if reason != "ImagePullBackOff" {
t.Fatalf("unexpected stuck container reason: %q", reason)
}
if reason := cluster.TestHookStuckContainerReason(nil, []string{"Running"}, []string{"CrashLoopBackOff"}); reason != "" {
t.Fatalf("expected no stuck reason, got %q", reason)
}
if reason := cluster.TestHookStuckVaultInitReason("Pending", true, 5*time.Minute, 10*time.Second); reason != "VaultInitStuck" {
t.Fatalf("expected VaultInitStuck, got %q", reason)
}
if reason := cluster.TestHookStuckVaultInitReason("Running", true, 5*time.Minute, 10*time.Second); reason != "" {
t.Fatalf("expected no vault-init stuck reason when phase is not Pending, got %q", reason)
}
if !cluster.TestHookPodTargetsIgnoredNode("titan-22", []string{"titan-22"}) {
t.Fatalf("expected ignored node match")
}
if cluster.TestHookPodTargetsIgnoredNode("titan-23", []string{"titan-22"}) {
t.Fatalf("expected ignored node mismatch")
}
if !cluster.TestHookWorkloadTargetsIgnoredNodes("titan-22", nil, []string{"titan-22"}) {
t.Fatalf("expected node selector ignore match")
}
if !cluster.TestHookWorkloadTargetsIgnoredNodes("", []string{"titan-22"}, []string{"titan-22"}) {
t.Fatalf("expected affinity-only ignored-node match")
}
if cluster.TestHookWorkloadTargetsIgnoredNodes("", []string{"titan-23"}, []string{"titan-22"}) {
t.Fatalf("expected affinity mismatch to be false")
}
if count := cluster.TestHookParseWorkloadIgnoreRulesCount([]string{"monitoring/grafana", "monitoring/deployment/kube-state-metrics"}); count != 2 {
t.Fatalf("unexpected workload ignore rule count: %d", count)
}
if !cluster.TestHookWorkloadIgnoredEntries([]string{"monitoring/grafana"}, "monitoring", "deployment", "grafana") {
t.Fatalf("expected workload ignored by rules")
}
if cluster.TestHookWorkloadIgnoredEntries([]string{"monitoring/grafana"}, "monitoring", "deployment", "loki") {
t.Fatalf("unexpected workload ignored by rules")
}
if idx := cluster.TestHookReadyConditionIndex([]string{"Progressing", "Ready"}); idx != 1 {
t.Fatalf("unexpected ready condition index: %d", idx)
}
if idx := cluster.TestHookReadyConditionIndex([]string{"Progressing"}); idx != -1 {
t.Fatalf("expected missing ready condition index=-1, got %d", idx)
}
if got := cluster.TestHookJoinLimited([]string{"a", "b", "c"}, 2); !strings.Contains(got, "+1 more") {
t.Fatalf("expected joinLimited truncation marker, got %q", got)
}
if got := cluster.TestHookJoinLimited([]string{"a", "b"}, 5); got != "a; b" {
t.Fatalf("unexpected joinLimited output: %q", got)
}
candidates := cluster.TestHookNamespaceCandidatesFromIgnoreKustomizations([]string{"flux-system/monitoring", "flux-system/gitea", "invalid"})
if len(candidates) != 2 || candidates[0] != "gitea" || candidates[1] != "monitoring" {
t.Fatalf("unexpected namespace candidates: %v", candidates)
}
}