CI / build (push) Successful in 30s
Apps opt in with sema.dashboard.* labels in their own compose file. The primary reads its host's containers through a read-only socket proxy, follows Docker events, and merges in what the agent on the second host reports. Icons come from the icon label, the dashboard-icons set or the app's favicon, with a letter as the fallback. Go with only the standard library, a distroless image, and a Gitea Actions workflow copied from FiMa's: checks in the Dockerfile's check stage, a Trivy scan, and a push to git.mariosem.es/msemes/hoda. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
113 lines
3.4 KiB
Go
113 lines
3.4 KiB
Go
// Package config reads HODA's settings from environment variables.
|
|
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Mode selects what an instance does: the primary serves the dashboard, an
|
|
// agent only reports its own host's apps to the primary.
|
|
type Mode string
|
|
|
|
const (
|
|
ModePrimary Mode = "primary"
|
|
ModeAgent Mode = "agent"
|
|
)
|
|
|
|
// Config holds every setting; the README lists what each one does.
|
|
type Config struct {
|
|
Mode Mode
|
|
Port string
|
|
DockerHost string
|
|
LabelPrefix string
|
|
Title string
|
|
NodeName string
|
|
Peers []string
|
|
PeerToken string
|
|
PeerInterval time.Duration
|
|
LinkTarget string
|
|
ResyncInterval time.Duration
|
|
IconRefresh time.Duration
|
|
IconCDN string
|
|
DataDir string
|
|
LogLevel slog.Level
|
|
}
|
|
|
|
// Load reads the configuration from the process environment.
|
|
func Load() (Config, error) {
|
|
return parse(os.Getenv)
|
|
}
|
|
|
|
func parse(getenv func(string) string) (Config, error) {
|
|
get := func(key, fallback string) string {
|
|
if v := strings.TrimSpace(getenv(key)); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|
|
c := Config{
|
|
Mode: Mode(strings.ToLower(get("MODE", string(ModePrimary)))),
|
|
Port: get("PORT", "8080"),
|
|
DockerHost: get("DOCKER_HOST", "unix:///var/run/docker.sock"),
|
|
LabelPrefix: strings.TrimSuffix(get("LABEL_PREFIX", "sema.dashboard"), "."),
|
|
Title: get("DASHBOARD_TITLE", "HODA"),
|
|
NodeName: get("NODE_NAME", ""),
|
|
PeerToken: get("PEER_TOKEN", ""),
|
|
LinkTarget: get("LINK_TARGET", "_blank"),
|
|
IconCDN: strings.TrimSuffix(get("ICON_CDN", "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons"), "/"),
|
|
DataDir: get("DATA_DIR", "/data"),
|
|
}
|
|
if c.NodeName == "" {
|
|
c.NodeName, _ = os.Hostname()
|
|
}
|
|
for _, p := range strings.Split(getenv("PEERS"), ",") {
|
|
if p = strings.TrimSuffix(strings.TrimSpace(p), "/"); p != "" {
|
|
c.Peers = append(c.Peers, p)
|
|
}
|
|
}
|
|
|
|
var errs []error
|
|
duration := func(key, fallback string) time.Duration {
|
|
raw := get(key, fallback)
|
|
d, err := time.ParseDuration(raw)
|
|
if err != nil || d <= 0 {
|
|
errs = append(errs, fmt.Errorf("%s=%q is not a positive duration such as 5s or 24h", key, raw))
|
|
}
|
|
return d
|
|
}
|
|
c.PeerInterval = duration("PEER_INTERVAL", "5s")
|
|
c.ResyncInterval = duration("RESYNC_INTERVAL", "60s")
|
|
c.IconRefresh = duration("ICON_REFRESH", "24h")
|
|
|
|
if err := c.LogLevel.UnmarshalText([]byte(get("LOG_LEVEL", "info"))); err != nil {
|
|
errs = append(errs, errors.New("LOG_LEVEL must be debug, info, warn or error"))
|
|
}
|
|
if c.Mode != ModePrimary && c.Mode != ModeAgent {
|
|
errs = append(errs, fmt.Errorf("MODE=%q must be primary or agent", c.Mode))
|
|
}
|
|
if n, err := strconv.Atoi(c.Port); err != nil || n < 1 || n > 65535 {
|
|
errs = append(errs, fmt.Errorf("PORT=%q is not a valid port", c.Port))
|
|
}
|
|
if c.LinkTarget != "_blank" && c.LinkTarget != "_self" {
|
|
errs = append(errs, fmt.Errorf("LINK_TARGET=%q must be _blank or _self", c.LinkTarget))
|
|
}
|
|
if c.Mode == ModeAgent && len(c.Peers) > 0 {
|
|
errs = append(errs, errors.New("PEERS only applies to MODE=primary"))
|
|
}
|
|
if (c.Mode == ModeAgent || len(c.Peers) > 0) && c.PeerToken == "" {
|
|
errs = append(errs, errors.New("PEER_TOKEN is required when MODE=agent or PEERS is set"))
|
|
}
|
|
for _, p := range c.Peers {
|
|
if !strings.HasPrefix(p, "http://") && !strings.HasPrefix(p, "https://") {
|
|
errs = append(errs, fmt.Errorf("PEERS entry %q must start with http:// or https://", p))
|
|
}
|
|
}
|
|
return c, errors.Join(errs...)
|
|
}
|