536 lines
10 KiB
Go
536 lines
10 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func writeTmp(t *testing.T, body string) string {
|
|
t.Helper()
|
|
p := filepath.Join(t.TempDir(), "c.toml")
|
|
if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
const minimal = `
|
|
agent_id = "host-1"
|
|
state_dir = "/tmp/x"
|
|
|
|
[server]
|
|
enabled = true
|
|
url = "http://localhost"
|
|
|
|
[[checks]]
|
|
id = "c1"
|
|
command = "true"
|
|
interval = "10s"
|
|
timeout = "5s"
|
|
`
|
|
|
|
func TestLoadMinimal(t *testing.T) {
|
|
c, err := Load(writeTmp(t, minimal))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !c.PushesToServer() || c.ExposesMetrics() {
|
|
t.Errorf("features: push=%v metrics=%v", c.PushesToServer(), c.ExposesMetrics())
|
|
}
|
|
if c.Server.HeartbeatInterval.Duration == 0 {
|
|
t.Error("default heartbeat not applied")
|
|
}
|
|
if !c.Checks[0].NotificationsOn() {
|
|
t.Error("notifications should default on")
|
|
}
|
|
}
|
|
|
|
func TestLoadCronCheck(t *testing.T) {
|
|
c, err := Load(writeTmp(t, `
|
|
agent_id = "host-1"
|
|
state_dir = "/tmp/x"
|
|
|
|
[server]
|
|
enabled = true
|
|
url = "http://localhost"
|
|
|
|
[[checks]]
|
|
id = "c1"
|
|
command = "true"
|
|
cron = "CRON_TZ=UTC 0 6 * * *"
|
|
timeout = "5m"
|
|
`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !c.Checks[0].UsesCron() || c.Checks[0].UsesInterval() {
|
|
t.Fatalf("unexpected schedule mode: %+v", c.Checks[0])
|
|
}
|
|
}
|
|
|
|
func TestValidateCheckScheduleChoice(t *testing.T) {
|
|
for name, body := range map[string]string{
|
|
"both_interval_and_cron": `
|
|
interval = "10s"
|
|
cron = "0 * * * *"
|
|
timeout = "5s"
|
|
`,
|
|
"neither_interval_nor_cron": `
|
|
timeout = "5s"
|
|
`,
|
|
"invalid_cron": `
|
|
cron = "not a cron"
|
|
timeout = "5s"
|
|
`,
|
|
"seconds_cron": `
|
|
cron = "0 0 * * * *"
|
|
timeout = "5s"
|
|
`,
|
|
"every_cron": `
|
|
cron = "@every 10s"
|
|
timeout = "5s"
|
|
`,
|
|
"tz_every_cron": `
|
|
cron = "CRON_TZ=UTC @every 10s"
|
|
timeout = "5s"
|
|
`,
|
|
"reboot_cron": `
|
|
cron = "@reboot"
|
|
timeout = "5s"
|
|
`,
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
_, err := Load(writeTmp(t, `
|
|
agent_id = "host-1"
|
|
state_dir = "/tmp/x"
|
|
|
|
[server]
|
|
enabled = true
|
|
url = "http://localhost"
|
|
|
|
[[checks]]
|
|
id = "c1"
|
|
command = "true"
|
|
`+body+`
|
|
`))
|
|
if err == nil {
|
|
t.Fatal("expected schedule validation error")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadResourceLimits(t *testing.T) {
|
|
c, err := Load(writeTmp(t, `
|
|
agent_id = "host-1"
|
|
state_dir = "/tmp/x"
|
|
|
|
[server]
|
|
enabled = true
|
|
url = "http://localhost"
|
|
|
|
[[checks]]
|
|
id = "c1"
|
|
command = "true"
|
|
interval = "10s"
|
|
timeout = "5s"
|
|
resource_limits = { cpu_time = "2s", memory = "256MiB", open_files = 128 }
|
|
`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
limits := c.Checks[0].ResourceLimits
|
|
if limits.CPUTime.Duration != 2_000_000_000 || limits.Memory.Bytes != 256*1024*1024 || limits.OpenFiles != 128 {
|
|
t.Fatalf("unexpected limits: %+v", limits)
|
|
}
|
|
}
|
|
|
|
func TestValidateBadResourceLimits(t *testing.T) {
|
|
for name, line := range map[string]string{
|
|
"memory_too_small": `resource_limits = { memory = "512KiB" }`,
|
|
"open_files_too_small": `resource_limits = { open_files = 8 }`,
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
_, err := Load(writeTmp(t, `
|
|
agent_id = "host-1"
|
|
state_dir = "/tmp/x"
|
|
|
|
[server]
|
|
enabled = true
|
|
url = "http://localhost"
|
|
|
|
[[checks]]
|
|
id = "c1"
|
|
command = "true"
|
|
interval = "10s"
|
|
timeout = "5s"
|
|
`+line+`
|
|
`))
|
|
if err == nil {
|
|
t.Fatal("expected resource limit error")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadMultilineCommand(t *testing.T) {
|
|
c, err := Load(writeTmp(t, `
|
|
agent_id = "host-1"
|
|
state_dir = "/tmp/x"
|
|
|
|
[server]
|
|
enabled = true
|
|
url = "http://localhost"
|
|
|
|
[[checks]]
|
|
id = "c1"
|
|
command = '''
|
|
set -eu
|
|
echo ok
|
|
'''
|
|
interval = "10s"
|
|
timeout = "5s"
|
|
`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(c.Checks[0].Command, "echo ok") {
|
|
t.Fatalf("command not loaded: %q", c.Checks[0].Command)
|
|
}
|
|
}
|
|
|
|
func TestCommandArrayRejected(t *testing.T) {
|
|
_, err := Load(writeTmp(t, `
|
|
agent_id = "host-1"
|
|
state_dir = "/tmp/x"
|
|
|
|
[server]
|
|
enabled = true
|
|
url = "http://localhost"
|
|
|
|
[[checks]]
|
|
id = "c1"
|
|
command = ["true"]
|
|
interval = "10s"
|
|
timeout = "5s"
|
|
`))
|
|
if err == nil {
|
|
t.Fatal("expected command array to be rejected")
|
|
}
|
|
}
|
|
|
|
func TestEmptyCommandRejected(t *testing.T) {
|
|
for name, cmd := range map[string]string{
|
|
"empty": `""`,
|
|
"whitespace": `" \t "`,
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
_, err := Load(writeTmp(t, `
|
|
agent_id = "host-1"
|
|
state_dir = "/tmp/x"
|
|
|
|
[server]
|
|
enabled = true
|
|
url = "http://localhost"
|
|
|
|
[[checks]]
|
|
id = "c1"
|
|
command = `+cmd+`
|
|
interval = "10s"
|
|
timeout = "5s"
|
|
`))
|
|
if err == nil {
|
|
t.Fatal("expected empty command to be rejected")
|
|
}
|
|
if !strings.Contains(err.Error(), "command is required") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadLabels(t *testing.T) {
|
|
c, err := Load(writeTmp(t, `
|
|
agent_id = "host-1"
|
|
state_dir = "/tmp/x"
|
|
|
|
[labels]
|
|
env = "prod"
|
|
role = "api"
|
|
|
|
[server]
|
|
enabled = true
|
|
url = "http://localhost"
|
|
|
|
[[checks]]
|
|
id = "c1"
|
|
command = "true"
|
|
interval = "10s"
|
|
timeout = "5s"
|
|
`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
labels := c.HeartbeatLabels("1.2.3")
|
|
if labels["env"] != "prod" || labels["role"] != "api" {
|
|
t.Fatalf("configured labels missing: %#v", labels)
|
|
}
|
|
if labels[AgentVersionLabel] != "1.2.3" {
|
|
t.Fatalf("version label missing: %#v", labels)
|
|
}
|
|
}
|
|
|
|
func TestHeartbeatLabelsVersionLabelWins(t *testing.T) {
|
|
c := &Config{Labels: map[string]string{AgentVersionLabel: "manual"}}
|
|
labels := c.HeartbeatLabels("real")
|
|
if labels[AgentVersionLabel] != "real" {
|
|
t.Fatalf("version label must be generated from binary version, got %q", labels[AgentVersionLabel])
|
|
}
|
|
}
|
|
|
|
func TestValidateBadID(t *testing.T) {
|
|
body := minimal + "\n"
|
|
_, err := Load(writeTmp(t, body[:0]+`
|
|
agent_id = "bad id!"
|
|
state_dir = "/tmp/x"
|
|
[server]
|
|
enabled = true
|
|
url = "http://localhost"
|
|
[[checks]]
|
|
id = "c1"
|
|
command = "true"
|
|
interval = "10s"
|
|
timeout = "5s"
|
|
`))
|
|
if err == nil {
|
|
t.Fatal("expected error for bad agent_id")
|
|
}
|
|
}
|
|
|
|
func TestValidateTimeoutExceedsInterval(t *testing.T) {
|
|
_, err := Load(writeTmp(t, `
|
|
agent_id = "x"
|
|
state_dir = "/tmp/x"
|
|
[server]
|
|
enabled = true
|
|
url = "http://localhost"
|
|
[[checks]]
|
|
id = "c1"
|
|
command = "true"
|
|
interval = "5s"
|
|
timeout = "10s"
|
|
`))
|
|
if err == nil {
|
|
t.Fatal("expected error for timeout > interval")
|
|
}
|
|
}
|
|
|
|
func TestValidateBadLabelKey(t *testing.T) {
|
|
_, err := Load(writeTmp(t, `
|
|
agent_id = "x"
|
|
state_dir = "/tmp/x"
|
|
[labels]
|
|
"bad key" = "x"
|
|
[server]
|
|
enabled = true
|
|
url = "http://localhost"
|
|
[[checks]]
|
|
id = "c1"
|
|
command = "true"
|
|
interval = "10s"
|
|
timeout = "5s"
|
|
`))
|
|
if err == nil {
|
|
t.Fatal("expected bad label key error")
|
|
}
|
|
}
|
|
|
|
func TestValidateSensitiveLabelKey(t *testing.T) {
|
|
for _, key := range []string{"token", "api_key", "api-key", "password", "credential", "authorization", "cookie"} {
|
|
if err := validateLabels(map[string]string{key: "x"}); err == nil {
|
|
t.Fatalf("expected sensitive label key error for %q", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateLabelsReservedSlot(t *testing.T) {
|
|
labels := make(map[string]string, maxLabels)
|
|
for i := 0; i < maxLabels; i++ {
|
|
labels[fmt.Sprintf("k%d", i)] = "v"
|
|
}
|
|
if err := validateLabels(labels); err == nil {
|
|
t.Fatal("expected reserved label slot error")
|
|
}
|
|
delete(labels, "k0")
|
|
labels[AgentVersionLabel] = "manual"
|
|
if err := validateLabels(labels); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestValidateLabelValueMaxLength(t *testing.T) {
|
|
if err := validateLabels(map[string]string{"note": strings.Repeat("ю", maxLabelValueLen)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := validateLabels(map[string]string{"note": strings.Repeat("x", maxLabelValueLen+1)}); err == nil {
|
|
t.Fatal("expected label value length error")
|
|
}
|
|
}
|
|
|
|
func TestPrometheusOnlyAllowsMissingServer(t *testing.T) {
|
|
c, err := Load(writeTmp(t, `
|
|
state_dir = "/tmp/x"
|
|
[server]
|
|
enabled = false
|
|
[metrics]
|
|
enabled = true
|
|
[[checks]]
|
|
id = "c1"
|
|
command = "true"
|
|
interval = "10s"
|
|
timeout = "5s"
|
|
`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.PushesToServer() {
|
|
t.Fatal("metrics-only config must not push")
|
|
}
|
|
if !c.ExposesMetrics() {
|
|
t.Fatal("metrics-only config must expose metrics")
|
|
}
|
|
}
|
|
|
|
func TestPushOnlyRequiresServer(t *testing.T) {
|
|
_, err := Load(writeTmp(t, `
|
|
state_dir = "/tmp/x"
|
|
[server]
|
|
enabled = true
|
|
[[checks]]
|
|
id = "c1"
|
|
command = "true"
|
|
interval = "10s"
|
|
timeout = "5s"
|
|
`))
|
|
if err == nil {
|
|
t.Fatal("push_only must require server.url")
|
|
}
|
|
}
|
|
|
|
func TestServerTokenRejected(t *testing.T) {
|
|
_, err := Load(writeTmp(t, `
|
|
state_dir = "/tmp/x"
|
|
[server]
|
|
enabled = true
|
|
url = "http://localhost"
|
|
token = "obsolete"
|
|
[[checks]]
|
|
id = "c1"
|
|
command = "true"
|
|
interval = "10s"
|
|
timeout = "5s"
|
|
`))
|
|
if err == nil {
|
|
t.Fatal("expected obsolete server.token to be rejected")
|
|
}
|
|
if !strings.Contains(err.Error(), "server.token") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestUnknownConfigKeyRejected(t *testing.T) {
|
|
_, err := Load(writeTmp(t, `
|
|
mode = "hybrid"
|
|
state_dir = "/tmp/x"
|
|
[server]
|
|
enabled = true
|
|
url = "http://localhost"
|
|
[[checks]]
|
|
id = "c1"
|
|
command = "true"
|
|
interval = "10s"
|
|
timeout = "5s"
|
|
`))
|
|
if err == nil {
|
|
t.Fatal("expected unknown key error")
|
|
}
|
|
}
|
|
|
|
func TestHostnameConfigKeyRejected(t *testing.T) {
|
|
_, err := Load(writeTmp(t, `
|
|
hostname = "host-1"
|
|
state_dir = "/tmp/x"
|
|
[server]
|
|
enabled = true
|
|
url = "http://localhost"
|
|
[[checks]]
|
|
id = "c1"
|
|
command = "true"
|
|
interval = "10s"
|
|
timeout = "5s"
|
|
`))
|
|
if err == nil {
|
|
t.Fatal("expected hostname key to be rejected")
|
|
}
|
|
if !strings.Contains(err.Error(), "hostname") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNotificationsEnabledCanDisable(t *testing.T) {
|
|
c, err := Load(writeTmp(t, minimal+`
|
|
notifications_enabled = false
|
|
`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.Checks[0].NotificationsOn() {
|
|
t.Fatal("notifications_enabled=false was not applied")
|
|
}
|
|
}
|
|
|
|
func TestExposesMetricsGate(t *testing.T) {
|
|
cases := []struct {
|
|
server bool
|
|
metrics bool
|
|
want bool
|
|
}{
|
|
{true, true, true},
|
|
{true, false, false},
|
|
{false, true, true},
|
|
{false, false, false},
|
|
}
|
|
for _, tc := range cases {
|
|
c := &Config{Server: ServerConfig{Enabled: &tc.server}, Metrics: MetricsConfig{Enabled: tc.metrics}}
|
|
if got := c.ExposesMetrics(); got != tc.want {
|
|
t.Errorf("server=%v metrics=%v want=%v got=%v", tc.server, tc.metrics, tc.want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateDuplicateCheckID(t *testing.T) {
|
|
_, err := Load(writeTmp(t, `
|
|
agent_id = "x"
|
|
state_dir = "/tmp/x"
|
|
[server]
|
|
enabled = true
|
|
url = "http://localhost"
|
|
[[checks]]
|
|
id = "c1"
|
|
command = "true"
|
|
interval = "10s"
|
|
timeout = "5s"
|
|
[[checks]]
|
|
id = "c1"
|
|
command = "true"
|
|
interval = "10s"
|
|
timeout = "5s"
|
|
`))
|
|
if err == nil {
|
|
t.Fatal("expected duplicate id error")
|
|
}
|
|
}
|