package alloy import ( "errors" "net" "os" "strings" "testing" "time" "zlh-agent/internal/state" ) const templateConfig = `logging { level = "info" } prometheus.exporter.unix "local" {} prometheus.scrape "local" { targets = prometheus.exporter.unix.local.targets forward_to = [prometheus.remote_write.zlh_monitor.receiver] } prometheus.remote_write "zlh_monitor" { endpoint { url = "http://10.60.0.25:9090/api/v1/write" } external_labels = { job = "old", instance = "old", collector = "old", role = "old", vmid = "old", } } ` func TestReplaceExternalLabelsBlockPreservesTemplate(t *testing.T) { labels := map[string]string{ "job": "integrations/unix", "instance": "10.200.0.46:12345", "collector": "alloy", "role": "game-container", "vmid": "5173", } rendered, err := ReplaceExternalLabelsBlock(templateConfig, labels) if err != nil { t.Fatalf("ReplaceExternalLabelsBlock: %v", err) } if !strings.Contains(rendered, `url = "http://10.60.0.25:9090/api/v1/write"`) { t.Fatalf("rendered config did not preserve template body:\n%s", rendered) } if !strings.Contains(rendered, `job = "integrations/unix",`) { t.Fatalf("rendered config missing job label:\n%s", rendered) } if !strings.Contains(rendered, `instance = "10.200.0.46:12345",`) { t.Fatalf("rendered config missing instance label:\n%s", rendered) } if strings.Contains(rendered, `job = "old"`) { t.Fatalf("rendered config retained old labels:\n%s", rendered) } } func TestLabelsDevAndGame(t *testing.T) { restoreTestHooks(t) devLabels, err := Labels(state.Config{ VMID: 6001, ContainerIP: "10.60.0.223", ContainerType: "dev", }) if err != nil { t.Fatalf("Labels dev: %v", err) } assertLabel(t, devLabels, "instance", "10.60.0.223:12345") assertLabel(t, devLabels, "role", "dev-container") assertLabel(t, devLabels, "vmid", "6001") gameLabels, err := Labels(state.Config{ VMID: 5001, ContainerIP: "10.60.0.224", ContainerType: "game", }) if err != nil { t.Fatalf("Labels game: %v", err) } assertLabel(t, gameLabels, "instance", "10.60.0.224:12345") assertLabel(t, gameLabels, "role", "game-container") assertLabel(t, gameLabels, "vmid", "5001") } func TestEnsureConfigUnchangedDoesNotRestart(t *testing.T) { restoreTestHooks(t) cfg := state.Config{ VMID: 6001, ContainerIP: "10.60.0.223", ContainerType: "dev", } labels, err := Labels(cfg) if err != nil { t.Fatalf("Labels: %v", err) } rendered, err := ReplaceExternalLabelsBlock(templateConfig, labels) if err != nil { t.Fatalf("ReplaceExternalLabelsBlock: %v", err) } restartCalls := 0 removeCalls := 0 writeFile = func(path string, data []byte, perm os.FileMode) error { if path != tmpConfigPath { t.Fatalf("write path = %q, want %q", path, tmpConfigPath) } return nil } readFile = func(path string) ([]byte, error) { if path != ConfigPath { t.Fatalf("read path = %q, want %q", path, ConfigPath) } return []byte(rendered), nil } removeFile = func(path string) error { removeCalls++ if path != tmpConfigPath { t.Fatalf("remove path = %q, want %q", path, tmpConfigPath) } return nil } renameFile = func(string, string) error { t.Fatalf("rename should not be called for unchanged config") return nil } runCommand = func(string, ...string) error { restartCalls++ return nil } result, err := EnsureConfig(cfg) if err != nil { t.Fatalf("EnsureConfig: %v", err) } if result.Applied { t.Fatalf("Applied = true, want false") } if removeCalls != 1 { t.Fatalf("removeCalls = %d, want 1", removeCalls) } if restartCalls != 0 { t.Fatalf("restartCalls = %d, want 0", restartCalls) } } func TestEnsureConfigChangedRestartsAndValidates(t *testing.T) { restoreTestHooks(t) restartCalls := 0 activeChecks := 0 listenChecks := 0 writeFile = func(path string, data []byte, perm os.FileMode) error { if path != tmpConfigPath { t.Fatalf("write path = %q, want %q", path, tmpConfigPath) } if !strings.Contains(string(data), `role = "game-container",`) { t.Fatalf("temp config missing rendered labels:\n%s", string(data)) } return nil } readFile = func(path string) ([]byte, error) { return []byte(templateConfig), nil } removeFile = func(string) error { return nil } renameFile = func(oldPath, newPath string) error { if oldPath != tmpConfigPath || newPath != ConfigPath { t.Fatalf("rename = %q -> %q, want %q -> %q", oldPath, newPath, tmpConfigPath, ConfigPath) } return nil } runCommand = func(name string, args ...string) error { if name != "systemctl" { t.Fatalf("command name = %q, want systemctl", name) } joined := strings.Join(args, " ") switch joined { case "restart alloy": restartCalls++ case "is-active --quiet alloy": activeChecks++ default: t.Fatalf("unexpected systemctl args: %s", joined) } return nil } dialTimeout = func(network, address string, timeout time.Duration) (net.Conn, error) { if network != "tcp" || address != "127.0.0.1:12345" { t.Fatalf("dial = %s %s, want tcp 127.0.0.1:12345", network, address) } listenChecks++ left, right := net.Pipe() _ = right.Close() return left, nil } result, err := EnsureConfig(state.Config{ VMID: 5001, ContainerIP: "10.60.0.224", ContainerType: "game", }) if err != nil { t.Fatalf("EnsureConfig: %v", err) } if !result.Applied { t.Fatalf("Applied = false, want true") } if restartCalls != 1 { t.Fatalf("restartCalls = %d, want 1", restartCalls) } if activeChecks != 1 { t.Fatalf("activeChecks = %d, want 1", activeChecks) } if listenChecks != 1 { t.Fatalf("listenChecks = %d, want 1", listenChecks) } } func TestEnsureConfigRetriesFailedUpdate(t *testing.T) { restoreTestHooks(t) attempts := 0 readFile = func(path string) ([]byte, error) { attempts++ if attempts == 1 { return nil, errors.New("temporary read failure") } return []byte(templateConfig), nil } writeFile = func(string, []byte, os.FileMode) error { return nil } removeFile = func(string) error { return nil } renameFile = func(string, string) error { return nil } runCommand = func(string, ...string) error { return nil } result, err := EnsureConfig(state.Config{ VMID: 5001, ContainerIP: "10.60.0.224", ContainerType: "game", }) if err != nil { t.Fatalf("EnsureConfig: %v", err) } if !result.Applied { t.Fatalf("Applied = false, want true after retry") } if attempts != 2 { t.Fatalf("attempts = %d, want 2", attempts) } } func TestEnsureConfigRetriesRestartAfterFileChanged(t *testing.T) { restoreTestHooks(t) restarts := 0 readFile = func(path string) ([]byte, error) { if restarts == 0 { return []byte(templateConfig), nil } labels, err := Labels(state.Config{VMID: 5001, ContainerIP: "10.60.0.224", ContainerType: "game"}) if err != nil { t.Fatalf("Labels: %v", err) } rendered, err := ReplaceExternalLabelsBlock(templateConfig, labels) if err != nil { t.Fatalf("ReplaceExternalLabelsBlock: %v", err) } return []byte(rendered), nil } writeFile = func(string, []byte, os.FileMode) error { return nil } removeFile = func(string) error { return nil } renameFile = func(string, string) error { return nil } runCommand = func(name string, args ...string) error { if name == "systemctl" && strings.Join(args, " ") == "restart alloy" { restarts++ if restarts == 1 { return errors.New("temporary restart failure") } } return nil } result, err := EnsureConfig(state.Config{ VMID: 5001, ContainerIP: "10.60.0.224", ContainerType: "game", }) if err != nil { t.Fatalf("EnsureConfig: %v", err) } if !result.Applied { t.Fatalf("Applied = false, want true after restart retry") } if restarts != 2 { t.Fatalf("restarts = %d, want 2", restarts) } } func assertLabel(t *testing.T, labels map[string]string, key, want string) { t.Helper() if got := labels[key]; got != want { t.Fatalf("labels[%q] = %q, want %q", key, got, want) } } func restoreTestHooks(t *testing.T) { t.Helper() oldLocalIP := localIPFunc oldRunCommand := runCommand oldDialTimeout := dialTimeout oldSleep := sleepFunc oldWriteFile := writeFile oldReadFile := readFile oldRemoveFile := removeFile oldRenameFile := renameFile t.Cleanup(func() { localIPFunc = oldLocalIP runCommand = oldRunCommand dialTimeout = oldDialTimeout sleepFunc = oldSleep writeFile = oldWriteFile readFile = oldReadFile removeFile = oldRemoveFile renameFile = oldRenameFile }) localIPFunc = func() (string, error) { return "10.60.0.200", nil } runCommand = func(string, ...string) error { return nil } dialTimeout = func(string, string, time.Duration) (net.Conn, error) { left, right := net.Pipe() _ = right.Close() return left, nil } sleepFunc = func(time.Duration) {} }