348 lines
8.7 KiB
Go
348 lines
8.7 KiB
Go
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) {}
|
|
}
|