internal/config/config.go

// 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
}