174 lines
10 KiB
Go
174 lines
10 KiB
Go
package config
|
|
|
|
import "testing"
|
|
|
|
// TestValidateRejectsInvalidFieldsMatrix runs one orchestration or CLI step.
|
|
// Signature: TestValidateRejectsInvalidFieldsMatrix(t *testing.T).
|
|
// Why: exercises validation branches so every config guardrail is test-covered.
|
|
func TestValidateRejectsInvalidFieldsMatrix(t *testing.T) {
|
|
base := defaults()
|
|
base.UPS.Target = "pyrphoros@localhost"
|
|
if err := base.Validate(); err != nil {
|
|
t.Fatalf("test baseline config must validate: %v", err)
|
|
}
|
|
cases := []struct {
|
|
name string
|
|
mutate func(*Config)
|
|
}{
|
|
{"empty_control_planes", func(c *Config) { c.ControlPlanes = nil }},
|
|
{"empty_expected_flux_branch", func(c *Config) { c.ExpectedFluxBranch = "" }},
|
|
{"empty_expected_flux_source", func(c *Config) { c.ExpectedFluxSource = "" }},
|
|
{"empty_iac_repo_path", func(c *Config) { c.IACRepoPath = "" }},
|
|
{"bad_shutdown_budget", func(c *Config) { c.Shutdown.DefaultBudgetSeconds = 0 }},
|
|
{"bad_shutdown_history_samples", func(c *Config) { c.Shutdown.HistoryMinSamples = 0 }},
|
|
{"bad_shutdown_emergency_budget", func(c *Config) { c.Shutdown.EmergencyBudgetSec = 0 }},
|
|
{"bad_shutdown_emergency_samples", func(c *Config) { c.Shutdown.EmergencyMinSamples = 0 }},
|
|
{"bad_drain_parallelism", func(c *Config) { c.Shutdown.DrainParallelism = 0 }},
|
|
{"bad_scale_parallelism", func(c *Config) { c.Shutdown.ScaleParallelism = 0 }},
|
|
{"bad_ssh_parallelism", func(c *Config) { c.Shutdown.SSHParallelism = 0 }},
|
|
{"bad_api_wait", func(c *Config) { c.Startup.APIWaitSeconds = 0 }},
|
|
{"bad_api_poll", func(c *Config) { c.Startup.APIPollSeconds = 0 }},
|
|
{"bad_min_battery_percent", func(c *Config) { c.Startup.MinimumBatteryPercent = 101 }},
|
|
{"bad_node_inventory_poll", func(c *Config) { c.Startup.NodeInventoryReachPollSeconds = 0 }},
|
|
{"bad_empty_node_inventory_required_node", func(c *Config) { c.Startup.NodeInventoryReachRequiredNodes = []string{"titan-0a", ""} }},
|
|
{"bad_time_sync_wait", func(c *Config) { c.Startup.TimeSyncWaitSeconds = 0 }},
|
|
{"bad_time_sync_poll", func(c *Config) { c.Startup.TimeSyncPollSeconds = 0 }},
|
|
{"bad_time_sync_quorum", func(c *Config) { c.Startup.TimeSyncMode = "quorum"; c.Startup.TimeSyncQuorum = 0 }},
|
|
{"bad_storage_wait", func(c *Config) { c.Startup.StorageReadyWaitSeconds = 0 }},
|
|
{"bad_storage_poll", func(c *Config) { c.Startup.StorageReadyPollSeconds = 0 }},
|
|
{"bad_storage_min_ready_nodes", func(c *Config) { c.Startup.StorageMinReadyNodes = 0 }},
|
|
{"bad_post_start_wait", func(c *Config) { c.Startup.PostStartProbeWaitSeconds = 0 }},
|
|
{"bad_post_start_poll", func(c *Config) { c.Startup.PostStartProbePollSeconds = 0 }},
|
|
{"bad_service_checklist_wait", func(c *Config) { c.Startup.ServiceChecklistWaitSeconds = 0 }},
|
|
{"bad_service_checklist_poll", func(c *Config) { c.Startup.ServiceChecklistPollSeconds = 0 }},
|
|
{"bad_service_checklist_stability", func(c *Config) { c.Startup.ServiceChecklistStabilitySec = -1 }},
|
|
{"bad_service_checklist_item_name", func(c *Config) {
|
|
c.Startup.ServiceChecklist = []ServiceChecklistCheck{{Name: "", URL: "https://ok", TimeoutSeconds: 5}}
|
|
}},
|
|
{"bad_service_checklist_item_timeout", func(c *Config) {
|
|
c.Startup.ServiceChecklist = []ServiceChecklistCheck{{Name: "x", URL: "https://ok", TimeoutSeconds: 0}}
|
|
}},
|
|
{"bad_service_checklist_http_code", func(c *Config) {
|
|
c.Startup.ServiceChecklist = []ServiceChecklistCheck{{Name: "x", URL: "https://ok", TimeoutSeconds: 5, AcceptedStatuses: []int{999}}}
|
|
}},
|
|
{"bad_critical_endpoint_wait", func(c *Config) { c.Startup.CriticalServiceEndpointWaitSec = 0 }},
|
|
{"bad_critical_endpoint_poll", func(c *Config) { c.Startup.CriticalServiceEndpointPollSec = 0 }},
|
|
{"bad_empty_critical_endpoints_when_required", func(c *Config) {
|
|
c.Startup.RequireCriticalServiceEndpoints = true
|
|
c.Startup.CriticalServiceEndpoints = nil
|
|
}},
|
|
{"bad_empty_critical_endpoint_entry", func(c *Config) {
|
|
c.Startup.CriticalServiceEndpoints = []string{"", "monitoring/victoria-metrics-single-server"}
|
|
}},
|
|
{"bad_malformed_critical_endpoint_entry", func(c *Config) {
|
|
c.Startup.CriticalServiceEndpoints = []string{"monitoring-victoria-metrics-single-server"}
|
|
}},
|
|
{"bad_ingress_wait", func(c *Config) { c.Startup.IngressChecklistWaitSeconds = 0 }},
|
|
{"bad_ingress_poll", func(c *Config) { c.Startup.IngressChecklistPollSeconds = 0 }},
|
|
{"bad_ingress_http_code", func(c *Config) { c.Startup.IngressChecklistAccepted = []int{700} }},
|
|
{"bad_ingress_ignore_entry", func(c *Config) { c.Startup.IngressChecklistIgnoreHosts = []string{"", "grafana.bstein.dev"} }},
|
|
{"bad_node_ssh_wait", func(c *Config) { c.Startup.NodeSSHAuthWaitSeconds = 0 }},
|
|
{"bad_node_ssh_poll", func(c *Config) { c.Startup.NodeSSHAuthPollSeconds = 0 }},
|
|
{"bad_empty_node_ssh_required_node", func(c *Config) { c.Startup.NodeSSHAuthRequiredNodes = []string{"titan-0a", ""} }},
|
|
{"bad_flux_wait", func(c *Config) { c.Startup.FluxHealthWaitSeconds = 0 }},
|
|
{"bad_flux_poll", func(c *Config) { c.Startup.FluxHealthPollSeconds = 0 }},
|
|
{"bad_empty_flux_required_kustomization", func(c *Config) { c.Startup.FluxHealthRequiredKustomizations = []string{"flux-system/core", ""} }},
|
|
{"bad_malformed_flux_required_kustomization", func(c *Config) { c.Startup.FluxHealthRequiredKustomizations = []string{"flux-system-core"} }},
|
|
{"bad_workload_wait", func(c *Config) { c.Startup.WorkloadConvergenceWaitSeconds = 0 }},
|
|
{"bad_workload_poll", func(c *Config) { c.Startup.WorkloadConvergencePollSeconds = 0 }},
|
|
{"bad_empty_required_workload_namespace", func(c *Config) { c.Startup.WorkloadConvergenceRequiredNamespaces = []string{"monitoring", ""} }},
|
|
{"bad_stuck_pod_grace", func(c *Config) { c.Startup.StuckPodGraceSeconds = 0 }},
|
|
{"bad_empty_post_start_probe_entry", func(c *Config) { c.Startup.PostStartProbes = []string{"https://ok", ""} }},
|
|
{"bad_empty_ignore_flux_entry", func(c *Config) { c.Startup.IgnoreFluxKustomizations = []string{"", "ns/name"} }},
|
|
{"bad_empty_ignore_workloads_entry", func(c *Config) { c.Startup.IgnoreWorkloads = []string{"", "ns/name"} }},
|
|
{"bad_empty_ignore_workload_namespaces_entry", func(c *Config) { c.Startup.IgnoreWorkloadNamespaces = []string{"", "vault"} }},
|
|
{"bad_overlap_flux_required_and_ignored", func(c *Config) {
|
|
c.Startup.FluxHealthRequiredKustomizations = []string{"flux-system/core"}
|
|
c.Startup.IgnoreFluxKustomizations = []string{"flux-system/core"}
|
|
}},
|
|
{"bad_overlap_workload_required_and_ignored", func(c *Config) {
|
|
c.Startup.WorkloadConvergenceRequiredNamespaces = []string{"monitoring"}
|
|
c.Startup.IgnoreWorkloadNamespaces = []string{"monitoring"}
|
|
}},
|
|
{"bad_empty_ignore_unavailable_nodes_entry", func(c *Config) { c.Startup.IgnoreUnavailableNodes = []string{"", "titan-22"} }},
|
|
{"bad_empty_vault_key_file", func(c *Config) { c.Startup.VaultUnsealKeyFile = "" }},
|
|
{"bad_scheduling_storm_threshold", func(c *Config) {
|
|
c.Startup.AutoQuarantineSchedulingStorms = true
|
|
c.Startup.SchedulingStormEventThreshold = 0
|
|
}},
|
|
{"bad_scheduling_storm_window", func(c *Config) {
|
|
c.Startup.AutoQuarantineSchedulingStorms = true
|
|
c.Startup.SchedulingStormWindowSeconds = 0
|
|
}},
|
|
{"bad_ssh_port_low", func(c *Config) { c.SSHPort = 0 }},
|
|
{"bad_ups_provider", func(c *Config) { c.UPS.Enabled = true; c.UPS.Provider = "" }},
|
|
{"bad_ups_on_battery_grace_negative", func(c *Config) { c.UPS.Enabled = true; c.UPS.OnBatteryGraceSeconds = -1 }},
|
|
{"bad_ups_target_empty", func(c *Config) { c.UPS.Enabled = true; c.UPS.Provider = "nut"; c.UPS.Target = ""; c.UPS.Targets = nil }},
|
|
{"bad_ups_targets_item_empty", func(c *Config) {
|
|
c.UPS.Enabled = true
|
|
c.UPS.Provider = "nut"
|
|
c.UPS.Target = ""
|
|
c.UPS.Targets = []UPSTarget{{Name: "x", Target: ""}}
|
|
}},
|
|
{"bad_startup_guard_age", func(c *Config) { c.Coordination.StartupGuardMaxAgeSec = 0 }},
|
|
{"bad_state_run_history_path", func(c *Config) { c.State.RunHistoryPath = "" }},
|
|
{"bad_state_intent_path", func(c *Config) { c.State.IntentPath = "" }},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cfg := base
|
|
tc.mutate(&cfg)
|
|
if err := cfg.Validate(); err == nil {
|
|
t.Fatalf("expected validation error for %s", tc.name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestApplyDefaultsPopulatesZeroConfig runs one orchestration or CLI step.
|
|
// Signature: TestApplyDefaultsPopulatesZeroConfig(t *testing.T).
|
|
// Why: validates that defaulting logic fills all critical runtime values.
|
|
func TestApplyDefaultsPopulatesZeroConfig(t *testing.T) {
|
|
var cfg Config
|
|
cfg.ControlPlanes = []string{"titan-0a", "titan-0b", "titan-0c"}
|
|
cfg.applyDefaults()
|
|
if cfg.ExpectedFluxBranch == "" || cfg.ExpectedFluxSource == "" || cfg.IACRepoPath == "" {
|
|
t.Fatalf("expected core defaults to be set")
|
|
}
|
|
if cfg.State.Dir == "" || cfg.State.ReportsDir == "" || cfg.State.RunHistoryPath == "" || cfg.State.LockPath == "" || cfg.State.IntentPath == "" {
|
|
t.Fatalf("expected state defaults to be set")
|
|
}
|
|
if cfg.Startup.TimeSyncMode == "" || cfg.Startup.EtcdRestoreControlPlane == "" || cfg.Startup.VaultUnsealKeyFile == "" {
|
|
t.Fatalf("expected startup defaults to be set")
|
|
}
|
|
if cfg.Startup.NodeInventoryReachRequiredNodes == nil || cfg.Startup.NodeSSHAuthRequiredNodes == nil ||
|
|
cfg.Startup.FluxHealthRequiredKustomizations == nil || cfg.Startup.WorkloadConvergenceRequiredNamespaces == nil {
|
|
t.Fatalf("expected startup recovery scope slices to be initialized")
|
|
}
|
|
if cfg.Startup.CriticalServiceEndpointWaitSec <= 0 || cfg.Startup.CriticalServiceEndpointPollSec <= 0 {
|
|
t.Fatalf("expected critical service endpoint timing defaults to be set")
|
|
}
|
|
if len(cfg.Startup.CriticalServiceEndpoints) == 0 {
|
|
t.Fatalf("expected critical service endpoint defaults to be set")
|
|
}
|
|
if cfg.Shutdown.SSHParallelism <= 0 || cfg.Shutdown.ScaleParallelism <= 0 || cfg.Shutdown.DrainParallelism <= 0 {
|
|
t.Fatalf("expected shutdown parallelism defaults to be set")
|
|
}
|
|
}
|
|
|
|
// TestValidateAcceptsFullySpecifiedConfig runs one orchestration or CLI step.
|
|
// Signature: TestValidateAcceptsFullySpecifiedConfig(t *testing.T).
|
|
// Why: covers success branches for forward-shutdown and UPS target validation.
|
|
func TestValidateAcceptsFullySpecifiedConfig(t *testing.T) {
|
|
cfg := defaults()
|
|
cfg.UPS.Target = "pyrphoros@localhost"
|
|
cfg.Coordination.ForwardShutdownHost = "titan-db"
|
|
cfg.Coordination.ForwardShutdownConfig = "/etc/ananke/ananke.yaml"
|
|
cfg.Coordination.PeerHosts = []string{"titan-24"}
|
|
if err := cfg.Validate(); err != nil {
|
|
t.Fatalf("expected fully specified config to validate: %v", err)
|
|
}
|
|
}
|