Harden production workflows and agent admission

This commit is contained in:
Stanislav Rossovskii
2026-05-28 14:19:27 +04:00
parent a2e88b4e76
commit 37b1a1d6d6
109 changed files with 4927 additions and 894 deletions

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"regexp"
"strconv"
"strings"
"time"
"unicode/utf8"
@@ -14,19 +15,24 @@ import (
var idPattern = regexp.MustCompile(`^[A-Za-z0-9._:-]+$`)
const (
maxIDLen = 128
maxLabelKeyLen = 64
maxLabelValueLen = 256
maxLabels = 32
maxIDLen = 128
maxLabelKeyLen = 64
maxLabelValueLen = 256
maxLabels = 32
// PH-019: bounded check inventory keeps {check_id} metric cardinality
// predictable. ValidateID already restricts each check_id to a stable
// identifier alphabet; this cap bounds the total. Override via
// MONLET_AGENT_MAX_CHECKS for fleets that genuinely need more.
defaultMaxChecks = 256
AgentVersionLabel = "monlet_agent_version"
defaultHeartbeat = 30 * time.Second
defaultHeartbeat = 10 * time.Second
defaultBatch = 10 * time.Second
defaultMetricsAddr = "127.0.0.1:9465"
)
type Config struct {
AgentID string `toml:"agent_id"`
Hostname string `toml:"hostname"`
Hostname string `toml:"-"`
StateDir string `toml:"state_dir"`
Labels map[string]string `toml:"labels"`
Server ServerConfig `toml:"server"`
@@ -37,7 +43,6 @@ type Config struct {
type ServerConfig struct {
Enabled *bool `toml:"enabled"`
URL string `toml:"url"`
Token string `toml:"token"`
HeartbeatInterval Duration `toml:"heartbeat_interval"`
BatchInterval Duration `toml:"batch_interval"`
}
@@ -124,11 +129,8 @@ func (c *Config) Validate() error {
if c.Server.URL == "" {
return fmt.Errorf("server.url is required when server.enabled = true")
}
if c.Server.Token == "" {
return fmt.Errorf("server.token is required when server.enabled = true")
}
} else if c.Server.URL != "" || c.Server.Token != "" {
return fmt.Errorf("server.url/token require server.enabled = true")
} else if c.Server.URL != "" {
return fmt.Errorf("server.url requires server.enabled = true")
}
if !c.PushesToServer() && !c.ExposesMetrics() {
return fmt.Errorf("server.enabled or metrics.enabled must be true")
@@ -136,6 +138,17 @@ func (c *Config) Validate() error {
if len(c.Checks) == 0 {
return fmt.Errorf("at least one check is required")
}
maxChecks := defaultMaxChecks
if v, ok := os.LookupEnv("MONLET_AGENT_MAX_CHECKS"); ok {
n, err := strconv.Atoi(v)
if err != nil || n <= 0 {
return fmt.Errorf("MONLET_AGENT_MAX_CHECKS must be a positive integer, got %q", v)
}
maxChecks = n
}
if len(c.Checks) > maxChecks {
return fmt.Errorf("too many checks: %d (max %d) — bounded for metric cardinality (PH-019)", len(c.Checks), maxChecks)
}
seen := make(map[string]struct{}, len(c.Checks))
for i := range c.Checks {
ch := &c.Checks[i]

View File

@@ -24,7 +24,6 @@ state_dir = "/tmp/x"
[server]
enabled = true
url = "http://localhost"
token = "t"
[[checks]]
id = "c1"
@@ -57,7 +56,6 @@ state_dir = "/tmp/x"
[server]
enabled = true
url = "http://localhost"
token = "t"
[[checks]]
id = "c1"
@@ -87,7 +85,6 @@ state_dir = "/tmp/x"
[server]
enabled = true
url = "http://localhost"
token = "t"
[[checks]]
id = "c1"
@@ -100,6 +97,36 @@ timeout = "5s"
}
}
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"
@@ -112,7 +139,6 @@ role = "api"
[server]
enabled = true
url = "http://localhost"
token = "t"
[[checks]]
id = "c1"
@@ -148,7 +174,6 @@ state_dir = "/tmp/x"
[server]
enabled = true
url = "http://localhost"
token = "t"
[[checks]]
id = "c1"
command = "true"
@@ -167,7 +192,6 @@ state_dir = "/tmp/x"
[server]
enabled = true
url = "http://localhost"
token = "t"
[[checks]]
id = "c1"
command = "true"
@@ -188,7 +212,6 @@ state_dir = "/tmp/x"
[server]
enabled = true
url = "http://localhost"
token = "t"
[[checks]]
id = "c1"
command = "true"
@@ -268,7 +291,28 @@ interval = "10s"
timeout = "5s"
`))
if err == nil {
t.Fatal("push_only must require server.url/token")
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)
}
}
@@ -279,7 +323,6 @@ state_dir = "/tmp/x"
[server]
enabled = true
url = "http://localhost"
token = "t"
[[checks]]
id = "c1"
command = "true"
@@ -291,6 +334,27 @@ timeout = "5s"
}
}
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
@@ -329,7 +393,6 @@ state_dir = "/tmp/x"
[server]
enabled = true
url = "http://localhost"
token = "t"
[[checks]]
id = "c1"
command = "true"