// Package config holds the top-level configuration for the portr tool.
//
// See mercemay.top/src/portr/ for context.
package config
import (
"fmt"
"time"
"github.com/mercemay/portr/internal/config/target"
)
// Config is what a user writes in their YAML file.
type Config struct {
Targets []target.Target `yaml:"targets"`
Concurrency int `yaml:"concurrency"`
Timeout time.Duration `yaml:"timeout"`
Retries int `yaml:"retries"`
RateLimit int `yaml:"rate_limit_per_second"`
Output OutputConfig `yaml:"output"`
}
// OutputConfig selects output sinks. At least one must be set.
type OutputConfig struct {
Table bool `yaml:"table"`
JSON bool `yaml:"json"`
Prometheus string `yaml:"prometheus_textfile"`
SlackWebhook string `yaml:"slack_webhook_env"`
}
// Defaults returns a Config populated with sensible defaults for a
// small homelab. Callers usually decode on top of this.
func Defaults() Config {
return Config{
Concurrency: 16,
Timeout: 2 * time.Second,
Retries: 1,
RateLimit: 50,
Output: OutputConfig{Table: true},
}
}
// Validate returns an error if the configuration is obviously wrong.
// It does not check reachability of any target.
func (c Config) Validate() error {
if len(c.Targets) == 0 {
return fmt.Errorf("config: no targets")
}
if c.Concurrency < 1 {
return fmt.Errorf("config: concurrency must be >= 1 (got %d)", c.Concurrency)
}
if c.Timeout <= 0 {
return fmt.Errorf("config: timeout must be positive")
}
if c.Retries < 0 {
return fmt.Errorf("config: retries must be >= 0")
}
if c.RateLimit < 0 {
return fmt.Errorf("config: rate_limit_per_second must be >= 0")
}
if !c.Output.Table && !c.Output.JSON && c.Output.Prometheus == "" && c.Output.SlackWebhook == "" {
return fmt.Errorf("config: no output sink selected")
}
seen := make(map[string]struct{}, len(c.Targets))
for _, t := range c.Targets {
key := t.Host + ":" + fmt.Sprint(t.Port)
if _, dup := seen[key]; dup {
return fmt.Errorf("config: duplicate target %s", key)
}
seen[key] = struct{}{}
if err := t.Validate(); err != nil {
return err
}
}
return nil
}