Add web auth, infinite-scroll, agent admission and review fixes across agent/server/web
This commit is contained in:
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -53,17 +54,28 @@ 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"`
|
||||
NotificationsEnabled *bool `toml:"notifications_enabled"`
|
||||
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"`
|
||||
ResourceLimits ResourceLimits `toml:"resource_limits"`
|
||||
}
|
||||
|
||||
type Duration struct{ time.Duration }
|
||||
|
||||
// ByteSize stores a parsed TOML byte size.
|
||||
type ByteSize struct{ Bytes int64 }
|
||||
|
||||
// ResourceLimits defines optional per-check OS resource limits.
|
||||
type ResourceLimits struct {
|
||||
CPUTime Duration `toml:"cpu_time"`
|
||||
Memory ByteSize `toml:"memory"`
|
||||
OpenFiles int `toml:"open_files"`
|
||||
}
|
||||
|
||||
func (d *Duration) UnmarshalText(b []byte) error {
|
||||
v, err := time.ParseDuration(string(b))
|
||||
if err != nil {
|
||||
@@ -73,6 +85,43 @@ func (d *Duration) UnmarshalText(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ByteSize) UnmarshalText(b []byte) error {
|
||||
raw := strings.TrimSpace(string(b))
|
||||
if raw == "" {
|
||||
return fmt.Errorf("empty byte size")
|
||||
}
|
||||
valueEnd := 0
|
||||
for valueEnd < len(raw) && ((raw[valueEnd] >= '0' && raw[valueEnd] <= '9') || raw[valueEnd] == '.') {
|
||||
valueEnd++
|
||||
}
|
||||
if valueEnd == 0 {
|
||||
return fmt.Errorf("invalid byte size %q", raw)
|
||||
}
|
||||
value, err := strconv.ParseFloat(raw[:valueEnd], 64)
|
||||
if err != nil || value <= 0 {
|
||||
return fmt.Errorf("invalid byte size %q", raw)
|
||||
}
|
||||
unit := strings.ToLower(strings.TrimSpace(raw[valueEnd:]))
|
||||
mul, ok := map[string]float64{
|
||||
"": 1, "b": 1,
|
||||
"k": 1000, "kb": 1000,
|
||||
"m": 1000 * 1000, "mb": 1000 * 1000,
|
||||
"g": 1000 * 1000 * 1000, "gb": 1000 * 1000 * 1000,
|
||||
"kib": 1024,
|
||||
"mib": 1024 * 1024,
|
||||
"gib": 1024 * 1024 * 1024,
|
||||
}[unit]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown byte size unit %q", unit)
|
||||
}
|
||||
bytes := value * mul
|
||||
if bytes > math.MaxInt64 {
|
||||
return fmt.Errorf("byte size %q is too large", raw)
|
||||
}
|
||||
s.Bytes = int64(bytes)
|
||||
return nil
|
||||
}
|
||||
|
||||
func Load(path string) (*Config, error) {
|
||||
var c Config
|
||||
md, err := toml.DecodeFile(path, &c)
|
||||
@@ -174,6 +223,9 @@ func (c *Config) Validate() error {
|
||||
if len(ch.DedupeKey) > 256 {
|
||||
return fmt.Errorf("check %q: dedupe_key too long", ch.ID)
|
||||
}
|
||||
if err := validateResourceLimits(ch.ID, ch.ResourceLimits); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -201,8 +253,23 @@ func (c CheckConfig) NotificationsOn() bool {
|
||||
return c.NotificationsEnabled == nil || *c.NotificationsEnabled
|
||||
}
|
||||
|
||||
func (c CheckConfig) Argv() []string {
|
||||
return []string{"/bin/sh", "-c", c.Command}
|
||||
func validateResourceLimits(checkID string, limits ResourceLimits) error {
|
||||
if limits.CPUTime.Duration < 0 {
|
||||
return fmt.Errorf("check %q: resource_limits.cpu_time must be > 0", checkID)
|
||||
}
|
||||
if limits.Memory.Bytes < 0 {
|
||||
return fmt.Errorf("check %q: resource_limits.memory must be > 0", checkID)
|
||||
}
|
||||
if limits.Memory.Bytes > 0 && limits.Memory.Bytes < 1024*1024 {
|
||||
return fmt.Errorf("check %q: resource_limits.memory must be >= 1MiB", checkID)
|
||||
}
|
||||
if limits.OpenFiles < 0 {
|
||||
return fmt.Errorf("check %q: resource_limits.open_files must be > 0", checkID)
|
||||
}
|
||||
if limits.OpenFiles > 0 && limits.OpenFiles < 16 {
|
||||
return fmt.Errorf("check %q: resource_limits.open_files must be >= 16", checkID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateID(field, v string) error {
|
||||
|
||||
@@ -48,6 +48,59 @@ func TestLoadMinimal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
@@ -72,9 +125,6 @@ timeout = "5s"
|
||||
if !strings.Contains(c.Checks[0].Command, "echo ok") {
|
||||
t.Fatalf("command not loaded: %q", c.Checks[0].Command)
|
||||
}
|
||||
if got := c.Checks[0].Argv(); len(got) != 3 || got[0] != "/bin/sh" || got[1] != "-c" {
|
||||
t.Fatalf("argv: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandArrayRejected(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user