Auto init via environment variables support, qu init for systemd
Container image / image (push) Successful in 1m38s
Container image / image (push) Successful in 1m38s
This commit is contained in:
@@ -3,10 +3,26 @@ package config
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Environment variable names that override fields on NodeConfig at
|
||||
// load time. Intended to let `docker compose` setups drive a node's
|
||||
// identity and listener configuration without having to bake a
|
||||
// node.yaml into the image or run `qu init` manually first.
|
||||
//
|
||||
// Empty values are ignored — they do not clear a field. The override
|
||||
// order is therefore: env (non-empty) > file > compiled default.
|
||||
const (
|
||||
EnvNodeID = "QUPTIME_NODE_ID"
|
||||
EnvBindAddr = "QUPTIME_BIND_ADDR"
|
||||
EnvBindPort = "QUPTIME_BIND_PORT"
|
||||
EnvAdvertise = "QUPTIME_ADVERTISE"
|
||||
EnvClusterSecret = "QUPTIME_CLUSTER_SECRET"
|
||||
)
|
||||
|
||||
// NodeConfig is the per-node, never-replicated identity file.
|
||||
type NodeConfig struct {
|
||||
// NodeID is a stable UUID generated at `qu init`. Used by all peers
|
||||
@@ -45,6 +61,34 @@ func (n *NodeConfig) AdvertiseAddr() string {
|
||||
return fmt.Sprintf("%s:%d", bind, n.BindPort)
|
||||
}
|
||||
|
||||
// ApplyEnvOverrides folds QUPTIME_* environment variables onto n.
|
||||
// Non-empty env values win over the existing field value. Called both
|
||||
// by LoadNodeConfig and by the `qu init` / serve auto-init paths so
|
||||
// the same precedence rules apply whether the daemon is reading a
|
||||
// persisted node.yaml or constructing one from scratch.
|
||||
func (n *NodeConfig) ApplyEnvOverrides() error {
|
||||
if v := os.Getenv(EnvNodeID); v != "" {
|
||||
n.NodeID = v
|
||||
}
|
||||
if v := os.Getenv(EnvBindAddr); v != "" {
|
||||
n.BindAddr = v
|
||||
}
|
||||
if v := os.Getenv(EnvBindPort); v != "" {
|
||||
p, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s=%q: not an integer: %w", EnvBindPort, v, err)
|
||||
}
|
||||
n.BindPort = p
|
||||
}
|
||||
if v := os.Getenv(EnvAdvertise); v != "" {
|
||||
n.Advertise = v
|
||||
}
|
||||
if v := os.Getenv(EnvClusterSecret); v != "" {
|
||||
n.ClusterSecret = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadNodeConfig reads node.yaml from the data dir.
|
||||
func LoadNodeConfig() (*NodeConfig, error) {
|
||||
raw, err := os.ReadFile(NodeFilePath())
|
||||
@@ -55,6 +99,9 @@ func LoadNodeConfig() (*NodeConfig, error) {
|
||||
if err := yaml.Unmarshal(raw, cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse node.yaml: %w", err)
|
||||
}
|
||||
if err := cfg.ApplyEnvOverrides(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cfg.BindPort == 0 {
|
||||
cfg.BindPort = 9901
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import "testing"
|
||||
|
||||
func TestAdvertiseAddrFallback(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
cfg NodeConfig
|
||||
want string
|
||||
name string
|
||||
cfg NodeConfig
|
||||
want string
|
||||
}{
|
||||
{"explicit advertise wins", NodeConfig{Advertise: "host:1234", BindAddr: "0.0.0.0", BindPort: 9901}, "host:1234"},
|
||||
{"empty bind falls back to loopback", NodeConfig{BindPort: 9901}, "127.0.0.1:9901"},
|
||||
@@ -56,3 +56,95 @@ func TestLoadNodeConfigAppliesDefaults(t *testing.T) {
|
||||
t.Errorf("BindAddr=%q want 0.0.0.0", loaded.BindAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvOverrides(t *testing.T) {
|
||||
t.Setenv(EnvNodeID, "node-from-env")
|
||||
t.Setenv(EnvBindAddr, "1.2.3.4")
|
||||
t.Setenv(EnvBindPort, "9999")
|
||||
t.Setenv(EnvAdvertise, "public.example.com:9999")
|
||||
t.Setenv(EnvClusterSecret, "shh-secret")
|
||||
|
||||
n := &NodeConfig{
|
||||
NodeID: "original-id",
|
||||
BindAddr: "0.0.0.0",
|
||||
BindPort: 9901,
|
||||
Advertise: "old.example.com:9901",
|
||||
ClusterSecret: "old-secret",
|
||||
}
|
||||
if err := n.ApplyEnvOverrides(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := NodeConfig{
|
||||
NodeID: "node-from-env",
|
||||
BindAddr: "1.2.3.4",
|
||||
BindPort: 9999,
|
||||
Advertise: "public.example.com:9999",
|
||||
ClusterSecret: "shh-secret",
|
||||
}
|
||||
if *n != want {
|
||||
t.Errorf("got %+v want %+v", *n, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesEmptyValuesIgnored(t *testing.T) {
|
||||
// Explicitly empty env vars must NOT clobber existing fields —
|
||||
// otherwise `docker run -e QUPTIME_ADVERTISE=` would silently
|
||||
// erase a previously-persisted advertise address.
|
||||
t.Setenv(EnvNodeID, "")
|
||||
t.Setenv(EnvBindAddr, "")
|
||||
t.Setenv(EnvBindPort, "")
|
||||
t.Setenv(EnvAdvertise, "")
|
||||
t.Setenv(EnvClusterSecret, "")
|
||||
|
||||
orig := NodeConfig{
|
||||
NodeID: "keep-me",
|
||||
BindAddr: "10.0.0.1",
|
||||
BindPort: 9901,
|
||||
Advertise: "keep.example.com:9901",
|
||||
ClusterSecret: "keep-secret",
|
||||
}
|
||||
n := orig
|
||||
if err := n.ApplyEnvOverrides(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != orig {
|
||||
t.Errorf("empty env vars mutated config: got %+v want %+v", n, orig)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesBadPort(t *testing.T) {
|
||||
t.Setenv(EnvBindPort, "not-an-int")
|
||||
n := &NodeConfig{}
|
||||
if err := n.ApplyEnvOverrides(); err == nil {
|
||||
t.Fatal("expected error for non-integer port")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadNodeConfigEnvOverridesFile(t *testing.T) {
|
||||
t.Setenv("QUPTIME_DIR", t.TempDir())
|
||||
// Persist a file with one bind addr; env should win on load.
|
||||
n := &NodeConfig{NodeID: "abc", BindAddr: "127.0.0.1", BindPort: 9901, Advertise: "file.example.com:9901"}
|
||||
if err := n.Save(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv(EnvBindAddr, "0.0.0.0")
|
||||
t.Setenv(EnvAdvertise, "env.example.com:9001")
|
||||
t.Setenv(EnvBindPort, "9001")
|
||||
|
||||
loaded, err := LoadNodeConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if loaded.BindAddr != "0.0.0.0" {
|
||||
t.Errorf("BindAddr=%q want 0.0.0.0 (env override)", loaded.BindAddr)
|
||||
}
|
||||
if loaded.BindPort != 9001 {
|
||||
t.Errorf("BindPort=%d want 9001 (env override)", loaded.BindPort)
|
||||
}
|
||||
if loaded.Advertise != "env.example.com:9001" {
|
||||
t.Errorf("Advertise=%q want env.example.com:9001 (env override)", loaded.Advertise)
|
||||
}
|
||||
if loaded.NodeID != "abc" {
|
||||
t.Errorf("NodeID=%q want abc (unchanged)", loaded.NodeID)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user