Implement Monlet MVP stack and UI updates
This commit is contained in:
@@ -4,7 +4,9 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
@@ -13,22 +15,27 @@ var idPattern = regexp.MustCompile(`^[A-Za-z0-9._:-]+$`)
|
||||
|
||||
const (
|
||||
maxIDLen = 128
|
||||
maxLabelKeyLen = 64
|
||||
maxLabelValueLen = 256
|
||||
maxLabels = 32
|
||||
AgentVersionLabel = "monlet_agent_version"
|
||||
defaultHeartbeat = 30 * time.Second
|
||||
defaultBatch = 10 * time.Second
|
||||
defaultMetricsAddr = "127.0.0.1:9465"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
AgentID string `toml:"agent_id"`
|
||||
Hostname string `toml:"hostname"`
|
||||
Mode string `toml:"mode"`
|
||||
StateDir string `toml:"state_dir"`
|
||||
Server ServerConfig `toml:"server"`
|
||||
Metrics MetricsConfig `toml:"metrics"`
|
||||
Checks []CheckConfig `toml:"checks"`
|
||||
AgentID string `toml:"agent_id"`
|
||||
Hostname string `toml:"hostname"`
|
||||
StateDir string `toml:"state_dir"`
|
||||
Labels map[string]string `toml:"labels"`
|
||||
Server ServerConfig `toml:"server"`
|
||||
Metrics MetricsConfig `toml:"metrics"`
|
||||
Checks []CheckConfig `toml:"checks"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Enabled *bool `toml:"enabled"`
|
||||
URL string `toml:"url"`
|
||||
Token string `toml:"token"`
|
||||
HeartbeatInterval Duration `toml:"heartbeat_interval"`
|
||||
@@ -41,13 +48,13 @@ type MetricsConfig struct {
|
||||
}
|
||||
|
||||
type CheckConfig struct {
|
||||
ID string `toml:"id"`
|
||||
Name string `toml:"name"`
|
||||
Command []string `toml:"command"`
|
||||
Interval Duration `toml:"interval"`
|
||||
Timeout Duration `toml:"timeout"`
|
||||
NotificationOwner string `toml:"notification_owner"`
|
||||
DedupeKey string `toml:"dedupe_key"`
|
||||
ID string `toml:"id"`
|
||||
Name string `toml:"name"`
|
||||
Command string `toml:"command"`
|
||||
Interval Duration `toml:"interval"`
|
||||
Timeout Duration `toml:"timeout"`
|
||||
NotificationsEnabled *bool `toml:"notifications_enabled"`
|
||||
DedupeKey string `toml:"dedupe_key"`
|
||||
}
|
||||
|
||||
type Duration struct{ time.Duration }
|
||||
@@ -63,9 +70,17 @@ func (d *Duration) UnmarshalText(b []byte) error {
|
||||
|
||||
func Load(path string) (*Config, error) {
|
||||
var c Config
|
||||
if _, err := toml.DecodeFile(path, &c); err != nil {
|
||||
md, err := toml.DecodeFile(path, &c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read config: %w", err)
|
||||
}
|
||||
if undecoded := md.Undecoded(); len(undecoded) > 0 {
|
||||
keys := make([]string, len(undecoded))
|
||||
for i, key := range undecoded {
|
||||
keys[i] = key.String()
|
||||
}
|
||||
return nil, fmt.Errorf("unknown config keys: %s", strings.Join(keys, ", "))
|
||||
}
|
||||
c.applyDefaults()
|
||||
if err := c.Validate(); err != nil {
|
||||
return nil, err
|
||||
@@ -74,9 +89,6 @@ func Load(path string) (*Config, error) {
|
||||
}
|
||||
|
||||
func (c *Config) applyDefaults() {
|
||||
if c.Mode == "" {
|
||||
c.Mode = "hybrid"
|
||||
}
|
||||
if c.Hostname == "" {
|
||||
if h, err := os.Hostname(); err == nil {
|
||||
c.Hostname = h
|
||||
@@ -99,21 +111,27 @@ func (c *Config) Validate() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
switch c.Mode {
|
||||
case "prometheus_only", "push_only", "hybrid":
|
||||
default:
|
||||
return fmt.Errorf("invalid mode %q", c.Mode)
|
||||
if c.Hostname == "" {
|
||||
return fmt.Errorf("hostname is required")
|
||||
}
|
||||
if c.StateDir == "" {
|
||||
return fmt.Errorf("state_dir is required")
|
||||
}
|
||||
if err := validateLabels(c.Labels); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.PushesToServer() {
|
||||
if c.Server.URL == "" {
|
||||
return fmt.Errorf("server.url is required for mode %q", c.Mode)
|
||||
return fmt.Errorf("server.url is required when server.enabled = true")
|
||||
}
|
||||
if c.Server.Token == "" {
|
||||
return fmt.Errorf("server.token is required for mode %q", c.Mode)
|
||||
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")
|
||||
}
|
||||
if !c.PushesToServer() && !c.ExposesMetrics() {
|
||||
return fmt.Errorf("server.enabled or metrics.enabled must be true")
|
||||
}
|
||||
if len(c.Checks) == 0 {
|
||||
return fmt.Errorf("at least one check is required")
|
||||
@@ -128,7 +146,7 @@ func (c *Config) Validate() error {
|
||||
return fmt.Errorf("duplicate check id %q", ch.ID)
|
||||
}
|
||||
seen[ch.ID] = struct{}{}
|
||||
if len(ch.Command) == 0 {
|
||||
if strings.TrimSpace(ch.Command) == "" {
|
||||
return fmt.Errorf("check %q: command is required", ch.ID)
|
||||
}
|
||||
if ch.Interval.Duration <= 0 {
|
||||
@@ -140,14 +158,6 @@ func (c *Config) Validate() error {
|
||||
if ch.Timeout.Duration > ch.Interval.Duration {
|
||||
return fmt.Errorf("check %q: timeout must be <= interval", ch.ID)
|
||||
}
|
||||
switch ch.NotificationOwner {
|
||||
case "", "server", "prometheus", "none":
|
||||
default:
|
||||
return fmt.Errorf("check %q: invalid notification_owner %q", ch.ID, ch.NotificationOwner)
|
||||
}
|
||||
if ch.NotificationOwner == "" {
|
||||
ch.NotificationOwner = "server"
|
||||
}
|
||||
if len(ch.DedupeKey) > 256 {
|
||||
return fmt.Errorf("check %q: dedupe_key too long", ch.ID)
|
||||
}
|
||||
@@ -155,15 +165,31 @@ func (c *Config) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PushesToServer is true when the mode requires heartbeat/events push.
|
||||
// PushesToServer is true when heartbeat/events push is enabled.
|
||||
func (c *Config) PushesToServer() bool {
|
||||
return c.Mode == "push_only" || c.Mode == "hybrid"
|
||||
return c.Server.Enabled != nil && *c.Server.Enabled
|
||||
}
|
||||
|
||||
// ExposesMetrics is true when the agent should serve /metrics.
|
||||
// `metrics.enabled` is an independent gate; both must agree.
|
||||
func (c *Config) ExposesMetrics() bool {
|
||||
return c.Metrics.Enabled && (c.Mode == "prometheus_only" || c.Mode == "hybrid")
|
||||
return c.Metrics.Enabled
|
||||
}
|
||||
|
||||
func (c *Config) HeartbeatLabels(version string) map[string]string {
|
||||
labels := make(map[string]string, len(c.Labels)+1)
|
||||
for k, v := range c.Labels {
|
||||
labels[k] = v
|
||||
}
|
||||
labels[AgentVersionLabel] = version
|
||||
return labels
|
||||
}
|
||||
|
||||
func (c CheckConfig) NotificationsOn() bool {
|
||||
return c.NotificationsEnabled == nil || *c.NotificationsEnabled
|
||||
}
|
||||
|
||||
func (c CheckConfig) Argv() []string {
|
||||
return []string{"/bin/sh", "-c", c.Command}
|
||||
}
|
||||
|
||||
func ValidateID(field, v string) error {
|
||||
@@ -178,3 +204,40 @@ func ValidateID(field, v string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateLabels(labels map[string]string) error {
|
||||
if len(labels) > maxLabels {
|
||||
return fmt.Errorf("labels exceed %d entries", maxLabels)
|
||||
}
|
||||
if _, hasVersion := labels[AgentVersionLabel]; !hasVersion && len(labels) >= maxLabels {
|
||||
return fmt.Errorf("labels leave no room for reserved %q label", AgentVersionLabel)
|
||||
}
|
||||
for k, v := range labels {
|
||||
if k == "" {
|
||||
return fmt.Errorf("label key is empty")
|
||||
}
|
||||
if len(k) > maxLabelKeyLen {
|
||||
return fmt.Errorf("label key %q exceeds %d chars", k, maxLabelKeyLen)
|
||||
}
|
||||
if !idPattern.MatchString(k) {
|
||||
return fmt.Errorf("label key %q has invalid characters", k)
|
||||
}
|
||||
if isSensitiveLabelKey(k) {
|
||||
return fmt.Errorf("label key %q is not allowed", k)
|
||||
}
|
||||
if utf8.RuneCountInString(v) > maxLabelValueLen {
|
||||
return fmt.Errorf("label %q value exceeds %d chars", k, maxLabelValueLen)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isSensitiveLabelKey(k string) bool {
|
||||
normalized := strings.NewReplacer("-", "_", ".", "_", ":", "_").Replace(strings.ToLower(k))
|
||||
for _, word := range []string{"token", "secret", "password", "credential", "authorization", "cookie", "api_key", "apikey"} {
|
||||
if strings.Contains(normalized, word) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user