2026-07-16 02:27:44 -03:00
package cluster
import (
"bufio"
"context"
"encoding/base64"
"errors"
"fmt"
"io"
"log"
"net"
"strings"
"testing"
"time"
"scm.bstein.dev/bstein/ananke/internal/config"
"scm.bstein.dev/bstein/ananke/internal/execx"
"scm.bstein.dev/bstein/ananke/internal/state"
)
// TestTCPServiceChecklistAndBackendHealBranches runs one orchestration or CLI step.
// Signature: TestTCPServiceChecklistAndBackendHealBranches(t *testing.T).
// Why: TCP health checks drive Mailu recovery paths and need branch coverage for
// banner matching, unnamed checks, duplicate backend suppression, and heal notes.
func TestTCPServiceChecklistAndBackendHealBranches ( t * testing . T ) {
addr := startTCPFixture ( t , "220 ready\r\n" , "250 pong\r\n" )
host , portText , err := net . SplitHostPort ( addr )
if err != nil {
t . Fatalf ( "split tcp fixture address: %v" , err )
}
var port int
if _ , err := fmt . Sscanf ( portText , "%d" , & port ) ; err != nil {
t . Fatalf ( "parse tcp fixture port: %v" , err )
}
cfg := config . Config {
Startup : config . Startup {
TCPServiceChecklist : [ ] config . TCPServiceChecklistCheck {
{ Name : "smtp" , Host : host , Port : port , Send : "PING\r\n" , ExpectContains : "pong" , Namespace : "mail" , Service : "mailu" } ,
} ,
} ,
}
orch := buildOrchestratorWithStubs ( t , cfg , nil )
ok , detail := orch . tcpServiceChecklistReady ( context . Background ( ) )
if ! ok || detail != "tcp-checks=1" {
t . Fatalf ( "expected tcp checklist success, ok=%v detail=%q" , ok , detail )
}
unnamed := config . TCPServiceChecklistCheck { Host : host , Port : port , ExpectContains : "missing" }
ok , detail = orch . tcpServiceCheckReady ( context . Background ( ) , unnamed )
if ok || ! strings . Contains ( detail , ` banner missing expected marker "missing" ` ) {
t . Fatalf ( "expected missing marker detail, ok=%v detail=%q" , ok , detail )
}
orch . cfg . Startup . TCPServiceChecklist = [ ] config . TCPServiceChecklistCheck { unnamed }
ok , detail = orch . tcpServiceChecklistReady ( context . Background ( ) )
if ok || ! strings . Contains ( detail , host + ":" + portText ) {
t . Fatalf ( "expected unnamed tcp check to use host:port detail, ok=%v detail=%q" , ok , detail )
}
healAddr := startTCPFixture ( t , "220 ready\r\n" , "250 pong\r\n" )
healHost , healPortText , _ := net . SplitHostPort ( healAddr )
var healPort int
_ , _ = fmt . Sscanf ( healPortText , "%d" , & healPort )
cfg . Startup . TCPServiceChecklist = [ ] config . TCPServiceChecklistCheck {
{ Host : healHost , Port : healPort , Namespace : "" , Service : "skip" } ,
{ Name : "ok" , Host : healHost , Port : healPort , ExpectContains : "ready" , Namespace : "mail" , Service : "mailu" } ,
{ Name : "bad" , Host : healHost , Port : healPort , ExpectContains : "absent" , Namespace : "mail" , Service : "mailu" } ,
{ Name : "dup" , Host : healHost , Port : healPort , ExpectContains : "absent" , Namespace : "mail" , Service : "mailu" } ,
}
var scaled , rolled bool
orch = buildOrchestratorWithStubs ( t , cfg , [ ] commandStub {
{ match : func ( name string , args [ ] string ) bool {
joined := strings . Join ( args , " " )
if name == "kubectl" && strings . Contains ( joined , "-n mail scale deployment mailu --replicas=1" ) {
scaled = true
return true
}
if name == "kubectl" && strings . Contains ( joined , "-n mail rollout status deployment/mailu" ) {
rolled = true
return true
}
return false
} } ,
{ match : matchContains ( "kubectl" , "-n" , "mail" , "scale" , "statefulset" , "mailu" ) , err : errors . New ( ` Error from server (NotFound): statefulsets.apps "mailu" not found ` ) } ,
} )
healed , err := orch . healFailedTCPServiceBackends ( context . Background ( ) )
if err != nil {
t . Fatalf ( "healFailedTCPServiceBackends: %v" , err )
}
if len ( healed ) != 1 || healed [ 0 ] != "mail/deployment/mailu" || ! scaled || ! rolled {
t . Fatalf ( "unexpected tcp backend heal result healed=%v scaled=%v rolled=%v" , healed , scaled , rolled )
}
lastAttempt := time . Now ( ) . Add ( - time . Minute )
orch . maybeAutoHealTCPServiceBackends ( context . Background ( ) , & lastAttempt )
}
// TestHostPrivilegeFailureAndSecretBranches runs one orchestration or CLI step.
// Signature: TestHostPrivilegeFailureAndSecretBranches(t *testing.T).
// Why: host repair depends on stable allowlist, secret, sudo, and failure
// classes before Ananke can safely restart or reboot managed nodes.
func TestHostPrivilegeFailureAndSecretBranches ( t * testing . T ) {
cfg := config . Config {
SSHManagedNodes : [ ] string { "titan-23" } ,
SSHUser : "atlas" ,
Startup : config . Startup {
HostSudoSecretNamespace : "host-secrets" ,
HostSudoSecretNameTemplate : "sudo-{node}" ,
HostSudoSecretPasswordKey : "password" ,
} ,
}
orch := buildOrchestratorWithStubs ( t , cfg , nil )
if _ , err := orch . runHostPrivilegedAction ( context . Background ( ) , " " , hostActionSudoPreflight , 0 ) ; err == nil {
t . Fatalf ( "expected empty node privilege error" )
}
if _ , err := orch . runHostPrivilegedAction ( context . Background ( ) , "unmanaged" , hostActionSudoPreflight , 0 ) ; err == nil {
t . Fatalf ( "expected unmanaged node privilege error" )
}
if _ , err := orch . runHostPrivilegedAction ( context . Background ( ) , "titan-23" , hostPrivilegedAction ( "bad-action" ) , 0 ) ; err == nil {
t . Fatalf ( "expected bad action privilege error" )
}
if got := orch . hostPrivilegedCommandTimeout ( ) ; got != 90 * time . Second {
t . Fatalf ( "expected default privileged timeout, got %s" , got )
}
orch . cfg . Startup . HostPrivilegedCommandTimeoutSec = 7
if got := orch . hostPrivilegedCommandTimeout ( ) ; got != 7 * time . Second {
t . Fatalf ( "expected configured privileged timeout, got %s" , got )
}
actions := [ ] struct {
action hostPrivilegedAction
args [ ] string
want string
} {
{ hostActionHostReboot , nil , "systemctl reboot" } ,
{ hostActionModprobeDMCrypt , nil , "modprobe dm_crypt" } ,
{ hostActionCrictlPods , nil , "crictl pods -o json" } ,
{ hostActionCrictlPs , nil , "crictl ps -a -o json" } ,
{ hostActionCrictlInspect , [ ] string { "abc123" } , "crictl inspect abc123" } ,
{ hostActionCrictlStop , [ ] string { "abc123" } , "crictl stop abc123" } ,
}
for _ , tc := range actions {
got , err := hostPrivilegedCommandArgs ( tc . action , tc . args ... )
if err != nil {
t . Fatalf ( "hostPrivilegedCommandArgs(%s): %v" , tc . action , err )
}
if strings . Join ( got , " " ) != tc . want {
t . Fatalf ( "hostPrivilegedCommandArgs(%s)=%q want %q" , tc . action , strings . Join ( got , " " ) , tc . want )
}
}
for _ , tc := range [ ] struct {
action hostPrivilegedAction
arg string
} {
{ hostActionCrictlInspect , "bad;id" } ,
{ hostActionCrictlStop , "" } ,
} {
if _ , err := hostPrivilegedCommandArgs ( tc . action , tc . arg ) ; err == nil {
t . Fatalf ( "expected unsafe arg rejection for %s" , tc . action )
}
}
errs := [ ] error {
classifyHostPrivilegeFailure ( "n" , hostActionSudoPreflight , context . DeadlineExceeded , "" ) ,
classifyHostPrivilegeFailure ( "n" , hostActionSudoPreflight , errors . New ( "try again" ) , "" ) ,
classifyHostPrivilegeFailure ( "n" , hostActionSudoPreflight , errors . New ( "permission denied publickey" ) , "" ) ,
classifyHostPrivilegeFailure ( "n" , hostActionSudoPreflight , errors . New ( "exit 1" ) , "line one\nline two" ) ,
}
wantClasses := [ ] string { "host-command-timeout" , "host-privilege-auth-failed" , "host-ssh-unavailable" , "host-command-failed" }
for i , err := range errs {
if ! strings . Contains ( err . Error ( ) , wantClasses [ i ] ) {
t . Fatalf ( "expected %s in %q" , wantClasses [ i ] , err )
}
}
secretJSON := ` { "data": { "password":" ` + base64 . StdEncoding . EncodeToString ( [ ] byte ( "pw\n" ) ) + ` "}} `
orch = buildOrchestratorWithStubs ( t , cfg , [ ] commandStub {
{ match : matchContains ( "ssh" , "sudo -n /usr/bin/systemctl --version" ) , out : "sudo: a password is required" , err : errors . New ( "sudo failed" ) } ,
{ match : matchContains ( "kubectl" , "-n" , "host-secrets" , "get" , "secret" , "sudo-titan-23" ) , out : secretJSON } ,
} )
orch . SetSSHInputOverride ( func ( _ context . Context , _ time . Duration , node , command , input string ) ( string , error ) {
if node != "titan-23" || ! strings . Contains ( command , "sudo -S" ) || input != "pw\n" {
t . Fatalf ( "unexpected password-backed ssh node=%q command=%q input=%q" , node , command , input )
}
return "incorrect password" , errors . New ( "try again" )
} )
if _ , err := orch . runHostPrivilegedAction ( context . Background ( ) , "titan-23" , hostActionSudoPreflight , 0 ) ; err == nil || ! strings . Contains ( err . Error ( ) , "host-privilege-auth-failed" ) {
t . Fatalf ( "expected password-backed auth failure, got %v" , err )
}
secretCases := [ ] struct {
name string
cfg config . Config
node string
out string
err error
class string
} {
{ name : "unconfigured" , cfg : config . Config { } , class : "secret-lookup-unconfigured" } ,
{ name : "empty-name" , cfg : config . Config { Startup : config . Startup { HostSudoSecretNamespace : "ns" , HostSudoSecretNameTemplate : "{node}" } } , node : " " , class : "secret-lookup-invalid" } ,
{ name : "query-error" , cfg : cfg , err : errors . New ( "denied" ) , class : "secret-lookup-failed" } ,
{ name : "bad-json" , cfg : cfg , out : "{" , class : "secret-decode-failed" } ,
{ name : "missing-key" , cfg : cfg , out : ` { "data": { "other":"cHc="}} ` , class : "secret-key-missing" } ,
{ name : "bad-base64" , cfg : cfg , out : ` { "data": { "password":"%"}} ` , class : "secret-decode-failed" } ,
{ name : "empty-password" , cfg : cfg , out : ` { "data": { "password":"Cg=="}} ` , class : "secret-empty" } ,
}
for _ , tc := range secretCases {
orch := buildOrchestratorWithStubs ( t , tc . cfg , [ ] commandStub {
{ match : matchContains ( "kubectl" , "get" , "secret" ) , out : tc . out , err : tc . err } ,
} )
node := tc . node
if node == "" {
node = "titan-23"
}
_ , class , err := orch . hostSudoPassword ( context . Background ( ) , node )
if err == nil || class != tc . class {
t . Fatalf ( "%s: expected class %q error, got class=%q err=%v" , tc . name , tc . class , class , err )
}
}
}
// TestImagePullDNSAndCredentialBranches runs one orchestration or CLI step.
// Signature: TestImagePullDNSAndCredentialBranches(t *testing.T).
// Why: image-pull blockers should be classified instead of blindly recycling
// pods when DNS or registry credentials are the real outage cause.
func TestImagePullDNSAndCredentialBranches ( t * testing . T ) {
eventJSON := ` { "items" : [
{ "type" : "Normal" , "reason" : "Failed" , "involvedObject" : { "kind" : "Pod" , "namespace" : "skip" , "name" : "normal" } , "message" : "lookup registry.bstein.dev no such host" } ,
{ "type" : "Warning" , "reason" : "Failed" , "involvedObject" : { "kind" : "Service" , "namespace" : "skip" , "name" : "svc" } , "message" : "lookup registry.bstein.dev no such host" } ,
{ "type" : "Warning" , "reason" : "Failed" , "involvedObject" : { "kind" : "Pod" , "namespace" : "" , "name" : "" } , "message" : "lookup registry.bstein.dev no such host" } ,
{ "type" : "Warning" , "reason" : "Failed" , "involvedObject" : { "kind" : "Pod" , "namespace" : "media" , "name" : "app" } , "message" : "failed to pull image: lookup registry.bstein.dev: no such host" } ,
{ "type" : "Warning" , "reason" : "FailedPull" , "metadata" : { "namespace" : "finance" } , "involvedObject" : { "kind" : "Pod" , "name" : "budget" } , "message" : "unauthorized: authentication required" } ,
{ "type" : "Warning" , "reason" : "FailedToRetrieveImagePullSecret" , "involvedObject" : { "kind" : "Pod" , "namespace" : "sso" , "name" : "keycloak" } , "message" : "unable to retrieve some image pull secrets" }
] } `
2026-10-03 02:00:17 -05:00
eventJSON , podsJSON := currentCredentialFixture ( t , eventJSON )
2026-07-16 02:27:44 -03:00
orch := buildOrchestratorWithStubs ( t , config . Config { } , [ ] commandStub {
2026-10-03 02:00:17 -05:00
{ match : matchContains ( "kubectl" , "get" , "pods" , "-A" ) , out : podsJSON } ,
2026-07-16 02:27:44 -03:00
{ match : matchContains ( "kubectl" , "get" , "events" , "-A" ) , out : eventJSON } ,
} )
dnsReasons , err := orch . imagePullDNSBlockerReasons ( context . Background ( ) )
if err != nil {
t . Fatalf ( "imagePullDNSBlockerReasons: %v" , err )
}
if dnsReasons [ "media/app" ] != "ImagePullDNSBlocker:registry.bstein.dev" {
t . Fatalf ( "unexpected DNS reasons: %#v" , dnsReasons )
}
credentialReasons , err := orch . imagePullCredentialBlockerReasons ( context . Background ( ) )
if err != nil {
t . Fatalf ( "imagePullCredentialBlockerReasons: %v" , err )
}
if credentialReasons [ "finance/budget" ] != "ImagePullCredentialBlocker:unauthorized" || credentialReasons [ "sso/keycloak" ] != "ImagePullCredentialBlocker:missing-pull-secret" {
t . Fatalf ( "unexpected credential reasons: %#v" , credentialReasons )
}
for _ , message := range [ ] string {
"invalid username/password" ,
"403 forbidden" ,
"pull access denied" ,
"registry returned something strange" ,
} {
if got := imagePullCredentialFailureClass ( "Failed" , message ) ; got == "" {
t . Fatalf ( "expected credential class for %q" , message )
}
}
if ! vaultSyncDeployment ( "worker" , map [ string ] string { "app" : "secret-vault-sync" } ) {
t . Fatalf ( "expected vault-sync label value to identify sync deployment" )
}
for _ , message := range [ ] string { "no resolver marker here" , "lookup " , "lookup []" } {
if got := imagePullRegistryHost ( message ) ; got != "unknown-registry" {
t . Fatalf ( "expected unknown registry for %q, got %q" , message , got )
}
}
orch = buildOrchestratorWithStubs ( t , config . Config { } , [ ] commandStub {
{ match : matchContains ( "kubectl" , "-n" , "finance" , "get" , "deployment" ) , out : ` { "items":[ { "metadata": { "name":"finance-vault-sync","labels": { }}}, { "metadata": { "name":"app","labels": { }}}]} ` } ,
{ match : matchContains ( "kubectl" , "-n" , "finance" , "rollout" , "restart" , "deployment" , "finance-vault-sync" ) , out : "" } ,
{ match : matchContains ( "kubectl" , "-n" , "finance" , "rollout" , "status" , "deployment/finance-vault-sync" ) , out : "" } ,
} )
repaired , err := orch . restartVaultSyncDeployments ( context . Background ( ) , "finance" )
if err != nil || len ( repaired ) != 1 || repaired [ 0 ] != "finance/deployment/finance-vault-sync" {
t . Fatalf ( "unexpected vault-sync repair repaired=%v err=%v" , repaired , err )
}
for _ , tc := range [ ] struct {
name string
out string
err error
} {
{ name : "query" , err : errors . New ( "api down" ) } ,
{ name : "decode" , out : "{" } ,
{ name : "missing" , out : ` { "items":[ { "metadata": { "name":"app","labels": { }}}]} ` } ,
{ name : "restart" , out : ` { "items":[ { "metadata": { "name":"vault-sync","labels": { }}}]} ` , err : errors . New ( "restart denied" ) } ,
} {
orch := buildOrchestratorWithStubs ( t , config . Config { } , [ ] commandStub {
{ match : matchContains ( "kubectl" , "-n" , "ns" , "get" , "deployment" ) , out : tc . out , err : mapRestartErr ( tc . name , "query" , tc . err ) } ,
{ match : matchContains ( "kubectl" , "-n" , "ns" , "rollout" , "restart" ) , err : mapRestartErr ( tc . name , "restart" , tc . err ) } ,
} )
if _ , err := orch . restartVaultSyncDeployments ( context . Background ( ) , "ns" ) ; err == nil {
t . Fatalf ( "%s: expected restartVaultSyncDeployments error" , tc . name )
}
}
}
// TestLonghornSelectorAndRuntimeRecoveryBranches runs one orchestration or CLI step.
// Signature: TestLonghornSelectorAndRuntimeRecoveryBranches(t *testing.T).
// Why: Longhorn manager drift and kubelet proxy recovery are safety-critical
// repair paths that should stay covered without a live cluster.
func TestLonghornSelectorAndRuntimeRecoveryBranches ( t * testing . T ) {
if selectorLabelString ( map [ string ] string { "b" : "2" , "a" : "1" } ) != "a=1,b=2" {
t . Fatalf ( "selector labels should be sorted" )
}
if podMatchesLabels ( nil , map [ string ] string { "app" : "longhorn" } ) {
t . Fatalf ( "nil pod labels must not match selector" )
}
if len ( missingSelectorLabels ( map [ string ] string { "app" : "old" } , map [ string ] string { "app" : "longhorn" } ) ) != 1 {
t . Fatalf ( "expected missing selector label" )
}
cfg := config . Config { }
var labeled bool
orch := buildOrchestratorWithStubs ( t , cfg , [ ] commandStub {
{ match : matchContains ( "kubectl" , "-n" , "longhorn-system" , "get" , "nodes.longhorn.io" , "-o" , "json" ) , out : ` { "items" : [
{ "metadata" : { "name" : "" } } ,
{ "metadata" : { "name" : "missing-kube" } , "status" : { "conditions" : [ { "type" : "Ready" , "status" : "False" , "reason" : "ManagerPodMissing" } ] } } ,
{ "metadata" : { "name" : "has-manager" } , "status" : { "conditions" : [ { "type" : "Ready" , "status" : "False" , "reason" : "ManagerPodMissing" } ] } } ,
{ "metadata" : { "name" : "ready" } , "status" : { "conditions" : [ { "type" : "Ready" , "status" : "True" , "reason" : "ManagerPodMissing" } ] } } ,
{ "metadata" : { "name" : "other-reason" } , "status" : { "conditions" : [ { "type" : "Ready" , "status" : "False" , "reason" : "DiskPressure" } ] } } ,
{ "metadata" : { "name" : "already-labelled" } , "status" : { "conditions" : [ { "type" : "Ready" , "status" : "False" , "reason" : "ManagerPodMissing" } ] } } ,
{ "metadata" : { "name" : "repair" } , "status" : { "conditions" : [ { "type" : "Ready" , "status" : "False" , "reason" : "ManagerPodMissing" } ] } }
] } ` } ,
{ match : matchContains ( "kubectl" , "-n" , "longhorn-system" , "get" , "daemonset" , "longhorn-manager" ) , out : ` { "spec": { "selector": { "matchLabels": { "longhorn-host":"true","skip":""}}}} ` } ,
{ match : matchContains ( "kubectl" , "get" , "nodes" , "-o" , "json" ) , out : ` { "items" : [
{ "metadata" : { "name" : "has-manager" , "labels" : { } } } ,
{ "metadata" : { "name" : "ready" , "labels" : { } } } ,
{ "metadata" : { "name" : "other-reason" , "labels" : { } } } ,
{ "metadata" : { "name" : "already-labelled" , "labels" : { "longhorn-host" : "true" } } } ,
{ "metadata" : { "name" : "repair" , "labels" : { } } }
] } ` } ,
{ match : matchContains ( "kubectl" , "-n" , "longhorn-system" , "get" , "pods" , "-o" , "json" , "-l" , "longhorn-host=true" ) , out : ` { "items":[ { "metadata": { "labels": { "longhorn-host":"true"}},"spec": { "nodeName":"has-manager"}}, { "metadata": { "labels": { "longhorn-host":"false"}},"spec": { "nodeName":"repair"}}, { "metadata": { "labels": { "longhorn-host":"true"}},"spec": { "nodeName":""}}]} ` } ,
{ match : func ( name string , args [ ] string ) bool {
if name == "kubectl" && strings . Contains ( strings . Join ( args , " " ) , "label node repair --overwrite longhorn-host=true" ) {
labeled = true
return true
}
return false
} , out : "" } ,
} )
repaired , err := orch . reconcileLonghornKubernetesReadiness ( context . Background ( ) )
if err != nil || repaired != 1 || ! labeled {
t . Fatalf ( "unexpected longhorn reconcile repaired=%d labeled=%v err=%v" , repaired , labeled , err )
}
errOrch := buildOrchestratorWithStubs ( t , cfg , [ ] commandStub {
{ match : matchContains ( "kubectl" , "-n" , "longhorn-system" , "get" , "nodes.longhorn.io" , "-o" , "json" ) , out : ` { "items":[ { "metadata": { "name":"repair"},"status": { "conditions":[ { "type":"Ready","status":"False","reason":"ManagerPodMissing"}]}}]} ` } ,
{ match : matchContains ( "kubectl" , "-n" , "longhorn-system" , "get" , "daemonset" , "longhorn-manager" ) , out : ` { "spec": { "selector": { "matchLabels": { "longhorn-host":"true"}}}} ` } ,
{ match : matchContains ( "kubectl" , "get" , "nodes" , "-o" , "json" ) , out : ` { "items":[ { "metadata": { "name":"repair","labels": { }}}]} ` } ,
{ match : matchContains ( "kubectl" , "-n" , "longhorn-system" , "get" , "pods" , "-o" , "json" ) , out : ` { "items":[]} ` } ,
{ match : matchContains ( "kubectl" , "label" , "node" , "repair" ) , err : errors . New ( "label denied" ) } ,
} )
if repaired , err := errOrch . reconcileLonghornKubernetesReadiness ( context . Background ( ) ) ; err == nil || repaired != 0 {
t . Fatalf ( "expected longhorn label error, repaired=%d err=%v" , repaired , err )
}
runtimeCfg := config . Config {
SSHManagedNodes : [ ] string { "titan-23" } ,
SSHUser : "atlas" ,
Startup : config . Startup {
NodeRuntimeRestartWaitSeconds : 1 ,
NodeRuntimeRebootWaitSeconds : 1 ,
HostRepairAllowReboot : true ,
} ,
}
orch = buildOrchestratorWithStubs ( t , runtimeCfg , [ ] commandStub {
{ match : matchContains ( "ssh" , "sudo -n systemctl --no-block restart k3s-agent" ) , out : "" } ,
{ match : matchContains ( "kubectl" , "wait" , "node/titan-23" ) , err : errors . New ( "not ready" ) } ,
{ match : matchContains ( "ssh" , "sudo -n systemctl show k3s-agent" ) , out : "ActiveState=deactivating\nSubState=stop-sigterm" } ,
{ match : matchContains ( "ssh" , "sudo -n systemctl reboot" ) , out : "" } ,
} )
if err := orch . recoverManagedNodeRuntime ( context . Background ( ) , "titan-23" , true , "proxy broken" ) ; err == nil || ! strings . Contains ( err . Error ( ) , "node did not recover after controlled reboot" ) {
t . Fatalf ( "expected reboot wait failure, got %v" , err )
}
if err := orch . recoverManagedNodeRuntime ( context . Background ( ) , " " , true , "" ) ; err == nil {
t . Fatalf ( "expected empty runtime node error" )
}
if ! k3sAgentStateRequiresReboot ( "Result=timeout" ) || k3sAgentStateRequiresReboot ( "ActiveState=active" ) {
t . Fatalf ( "unexpected k3s-agent reboot predicate" )
}
if summarizeSystemctlShow ( "" ) != "unknown" || ! strings . Contains ( summarizeSystemctlShow ( "ActiveState=activating\nSubState=start" ) , "ActiveState" ) {
t . Fatalf ( "unexpected systemctl summary" )
}
}
// startTCPFixture runs one orchestration or CLI step.
// Signature: startTCPFixture(t *testing.T, banner string, reply string) string.
// Why: TCP checklist tests need a local loopback endpoint that emits the same
// banner/response shape as SMTP-like protocol probes.
func startTCPFixture ( t * testing . T , banner string , reply string ) string {
t . Helper ( )
listener , err := net . Listen ( "tcp" , "127.0.0.1:0" )
if err != nil {
t . Fatalf ( "listen tcp fixture: %v" , err )
}
t . Cleanup ( func ( ) { _ = listener . Close ( ) } )
go func ( ) {
for {
conn , err := listener . Accept ( )
if err != nil {
return
}
go func ( ) {
defer conn . Close ( )
_ , _ = conn . Write ( [ ] byte ( banner ) )
_ = conn . SetDeadline ( time . Now ( ) . Add ( 2 * time . Second ) )
_ , _ = bufio . NewReader ( conn ) . ReadString ( '\n' )
_ , _ = conn . Write ( [ ] byte ( reply ) )
} ( )
}
} ( )
return listener . Addr ( ) . String ( )
}
// mapRestartErr runs one orchestration or CLI step.
// Signature: mapRestartErr(caseName, target string, err error) error.
// Why: table-driven deployment restart tests need concise error injection
// without repeating one-off closures for each command phase.
func mapRestartErr ( caseName , target string , err error ) error {
if caseName == target {
return err
}
return nil
}
// TestLocalOrchestratorConstructorUsesQuietLogger runs one orchestration or CLI step.
// Signature: TestLocalOrchestratorConstructorUsesQuietLogger(t *testing.T).
// Why: keep imports in this closeout file anchored to real construction helpers
// while preserving package-local test readability.
func TestLocalOrchestratorConstructorUsesQuietLogger ( t * testing . T ) {
orch := & Orchestrator {
cfg : config . Config { } ,
runner : & execx . Runner { } ,
store : state . New ( t . TempDir ( ) + "/runs.json" ) ,
log : log . New ( io . Discard , "" , 0 ) ,
}
if orch . log == nil || orch . runner == nil || orch . store == nil {
t . Fatalf ( "expected constructed orchestrator test fixture" )
}
}