detached 68f340d

Add global DNS config defaults

commit
68f340dd5f3559e8f43da504d80aba8ccf07e3b7
author
brice · · 2026-05-01 13:40 UTC
parent
6a38a35
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +201 -0
M internal/app/acme_config_test.go +3 -0
@@ -15,4 +15,7 @@ func TestLoadConfig_ACMEDefaults(t *testing.T) {
1515 assert.Equal(t, "", v.GetString("tls.acme.email"))
1616 assert.Equal(t, "auto", v.GetString("tls.acme.challenge"))
1717 assert.Equal(t, 1, v.GetInt("tls.acme.obtain_batch_size"))
18+ assert.Equal(t, []string{"1.1.1.1:53", "8.8.8.8:53"}, v.GetStringSlice("dns.resolvers"))
19+ assert.Equal(t, "5m", v.GetString("dns.propagation_timeout"))
20+ assert.Equal(t, "5s", v.GetString("dns.polling_interval"))
1821 }
@@ -15,4 +15,7 @@ func TestLoadConfig_ACMEDefaults(t *testing.T) {
15 assert.Equal(t, "", v.GetString("tls.acme.email"))15 assert.Equal(t, "", v.GetString("tls.acme.email"))
16 assert.Equal(t, "auto", v.GetString("tls.acme.challenge"))16 assert.Equal(t, "auto", v.GetString("tls.acme.challenge"))
17 assert.Equal(t, 1, v.GetInt("tls.acme.obtain_batch_size"))17 assert.Equal(t, 1, v.GetInt("tls.acme.obtain_batch_size"))
18+ assert.Equal(t, []string{"1.1.1.1:53", "8.8.8.8:53"}, v.GetStringSlice("dns.resolvers"))
19+ assert.Equal(t, "5m", v.GetString("dns.propagation_timeout"))
20+ assert.Equal(t, "5s", v.GetString("dns.polling_interval"))
18 }21 }
M internal/app/run.go +47 -0
@@ -204,6 +204,12 @@ type Config struct {
204204 ObtainBatchSize int `mapstructure:"obtain_batch_size"`
205205 } `mapstructure:"acme"`
206206 } `mapstructure:"tls"`
207+
208+ DNS struct {
209+ Resolvers []string `mapstructure:"resolvers"`
210+ PropagationTimeout string `mapstructure:"propagation_timeout"`
211+ PollingInterval string `mapstructure:"polling_interval"`
212+ } `mapstructure:"dns"`
207213 }
208214
209215 // services holds all the services used by the application.
@@ -1748,6 +1754,44 @@ func buildProxyConfig(cfg Config, log zerowrap.Logger) (*proxyConfigResult, erro
17481754 }, nil
17491755 }
17501756
1757+// buildDNSConfig parses the raw dns config section into a publictls.DNSConfig.
1758+func buildDNSConfig(cfg Config) (publictls.DNSConfig, error) {
1759+ defaults := publictls.DefaultDNSConfig()
1760+
1761+ resolvers := cfg.DNS.Resolvers
1762+ if len(resolvers) == 0 {
1763+ resolvers = defaults.Resolvers
1764+ }
1765+
1766+ propagationTimeout := defaults.PropagationTimeout
1767+ if cfg.DNS.PropagationTimeout != "" {
1768+ parsed, err := time.ParseDuration(cfg.DNS.PropagationTimeout)
1769+ if err != nil {
1770+ return publictls.DNSConfig{}, fmt.Errorf("invalid dns.propagation_timeout: %w", err)
1771+ }
1772+ propagationTimeout = parsed
1773+ }
1774+
1775+ pollingInterval := defaults.PollingInterval
1776+ if cfg.DNS.PollingInterval != "" {
1777+ parsed, err := time.ParseDuration(cfg.DNS.PollingInterval)
1778+ if err != nil {
1779+ return publictls.DNSConfig{}, fmt.Errorf("invalid dns.polling_interval: %w", err)
1780+ }
1781+ pollingInterval = parsed
1782+ }
1783+
1784+ dnsCfg := publictls.DNSConfig{
1785+ Resolvers: append([]string(nil), resolvers...),
1786+ PropagationTimeout: propagationTimeout,
1787+ PollingInterval: pollingInterval,
1788+ }
1789+ if err := dnsCfg.Validate(); err != nil {
1790+ return publictls.DNSConfig{}, err
1791+ }
1792+ return dnsCfg, nil
1793+}
1794+
17511795 // createContainerService creates the container service with configuration.
17521796 func createContainerService(ctx context.Context, v *viper.Viper, cfg Config, svc *services, log zerowrap.Logger) (*container.Service, error) {
17531797 // Parse and validate container resource limits from config
@@ -3269,6 +3313,9 @@ func loadConfig(v *viper.Viper, configPath string) error {
32693313 v.SetDefault("tls.acme.email", "")
32703314 v.SetDefault("tls.acme.challenge", "auto")
32713315 v.SetDefault("tls.acme.obtain_batch_size", 1)
3316+ v.SetDefault("dns.resolvers", publictls.DefaultDNSResolvers)
3317+ v.SetDefault("dns.propagation_timeout", "5m")
3318+ v.SetDefault("dns.polling_interval", "5s")
32723319 v.SetDefault("server.force_https_redirect", false)
32733320 v.SetDefault("server.data_dir", DefaultDataDir())
32743321 v.SetDefault("server.runtime", "auto")
@@ -204,6 +204,12 @@ type Config struct {
204 ObtainBatchSize int `mapstructure:"obtain_batch_size"`204 ObtainBatchSize int `mapstructure:"obtain_batch_size"`
205 } `mapstructure:"acme"`205 } `mapstructure:"acme"`
206 } `mapstructure:"tls"`206 } `mapstructure:"tls"`
207+
208+ DNS struct {
209+ Resolvers []string `mapstructure:"resolvers"`
210+ PropagationTimeout string `mapstructure:"propagation_timeout"`
211+ PollingInterval string `mapstructure:"polling_interval"`
212+ } `mapstructure:"dns"`
207 }213 }
208 214
209 // services holds all the services used by the application.215 // services holds all the services used by the application.
@@ -1748,6 +1754,44 @@ func buildProxyConfig(cfg Config, log zerowrap.Logger) (*proxyConfigResult, erro
1748 }, nil1754 }, nil
1749 }1755 }
1750 1756
1757+// buildDNSConfig parses the raw dns config section into a publictls.DNSConfig.
1758+func buildDNSConfig(cfg Config) (publictls.DNSConfig, error) {
1759+ defaults := publictls.DefaultDNSConfig()
1760+
1761+ resolvers := cfg.DNS.Resolvers
1762+ if len(resolvers) == 0 {
1763+ resolvers = defaults.Resolvers
1764+ }
1765+
1766+ propagationTimeout := defaults.PropagationTimeout
1767+ if cfg.DNS.PropagationTimeout != "" {
1768+ parsed, err := time.ParseDuration(cfg.DNS.PropagationTimeout)
1769+ if err != nil {
1770+ return publictls.DNSConfig{}, fmt.Errorf("invalid dns.propagation_timeout: %w", err)
1771+ }
1772+ propagationTimeout = parsed
1773+ }
1774+
1775+ pollingInterval := defaults.PollingInterval
1776+ if cfg.DNS.PollingInterval != "" {
1777+ parsed, err := time.ParseDuration(cfg.DNS.PollingInterval)
1778+ if err != nil {
1779+ return publictls.DNSConfig{}, fmt.Errorf("invalid dns.polling_interval: %w", err)
1780+ }
1781+ pollingInterval = parsed
1782+ }
1783+
1784+ dnsCfg := publictls.DNSConfig{
1785+ Resolvers: append([]string(nil), resolvers...),
1786+ PropagationTimeout: propagationTimeout,
1787+ PollingInterval: pollingInterval,
1788+ }
1789+ if err := dnsCfg.Validate(); err != nil {
1790+ return publictls.DNSConfig{}, err
1791+ }
1792+ return dnsCfg, nil
1793+}
1794+
1751 // createContainerService creates the container service with configuration.1795 // createContainerService creates the container service with configuration.
1752 func createContainerService(ctx context.Context, v *viper.Viper, cfg Config, svc *services, log zerowrap.Logger) (*container.Service, error) {1796 func createContainerService(ctx context.Context, v *viper.Viper, cfg Config, svc *services, log zerowrap.Logger) (*container.Service, error) {
1753 // Parse and validate container resource limits from config1797 // Parse and validate container resource limits from config
@@ -3269,6 +3313,9 @@ func loadConfig(v *viper.Viper, configPath string) error {
3269 v.SetDefault("tls.acme.email", "")3313 v.SetDefault("tls.acme.email", "")
3270 v.SetDefault("tls.acme.challenge", "auto")3314 v.SetDefault("tls.acme.challenge", "auto")
3271 v.SetDefault("tls.acme.obtain_batch_size", 1)3315 v.SetDefault("tls.acme.obtain_batch_size", 1)
3316+ v.SetDefault("dns.resolvers", publictls.DefaultDNSResolvers)
3317+ v.SetDefault("dns.propagation_timeout", "5m")
3318+ v.SetDefault("dns.polling_interval", "5s")
3272 v.SetDefault("server.force_https_redirect", false)3319 v.SetDefault("server.force_https_redirect", false)
3273 v.SetDefault("server.data_dir", DefaultDataDir())3320 v.SetDefault("server.data_dir", DefaultDataDir())
3274 v.SetDefault("server.runtime", "auto")3321 v.SetDefault("server.runtime", "auto")
M internal/usecase/publictls/config.go +1 -0
@@ -17,6 +17,7 @@ type Config struct {
1717 TLSPort int
1818 DataDir string
1919 ObtainBatchSize int
20+ DNS DNSConfig
2021 }
2122
2223 // EffectiveChallenge represents the resolved ACME challenge configuration.
@@ -17,6 +17,7 @@ type Config struct {
17 TLSPort int17 TLSPort int
18 DataDir string18 DataDir string
19 ObtainBatchSize int19 ObtainBatchSize int
20+ DNS DNSConfig
20 }21 }
21 22
22 // EffectiveChallenge represents the resolved ACME challenge configuration.23 // EffectiveChallenge represents the resolved ACME challenge configuration.
A internal/usecase/publictls/dns_config.go +59 -0
@@ -0,0 +1,59 @@
1+package publictls
2+
3+import (
4+ "fmt"
5+ "net"
6+ "strings"
7+ "time"
8+)
9+
10+const (
11+ DefaultDNSPropagationTimeout = 5 * time.Minute
12+ DefaultDNSPollingInterval = 5 * time.Second
13+)
14+
15+var DefaultDNSResolvers = []string{"1.1.1.1:53", "8.8.8.8:53"}
16+
17+// DNSConfig controls public DNS visibility checks used by ACME DNS-01.
18+// These are recursive resolvers, not authoritative DNS providers.
19+type DNSConfig struct {
20+ Resolvers []string
21+ PropagationTimeout time.Duration
22+ PollingInterval time.Duration
23+}
24+
25+// DefaultDNSConfig returns Gordon's default public DNS check configuration.
26+func DefaultDNSConfig() DNSConfig {
27+ resolvers := append([]string(nil), DefaultDNSResolvers...)
28+ return DNSConfig{
29+ Resolvers: resolvers,
30+ PropagationTimeout: DefaultDNSPropagationTimeout,
31+ PollingInterval: DefaultDNSPollingInterval,
32+ }
33+}
34+
35+// Validate checks DNSConfig for ACME DNS-01 usage.
36+func (c DNSConfig) Validate() error {
37+ if len(c.Resolvers) == 0 {
38+ return fmt.Errorf("dns.resolvers must contain at least one resolver")
39+ }
40+ for i, resolver := range c.Resolvers {
41+ resolver = strings.TrimSpace(resolver)
42+ if resolver == "" {
43+ return fmt.Errorf("invalid dns.resolvers[%d]: empty resolver", i)
44+ }
45+ if _, _, err := net.SplitHostPort(resolver); err != nil {
46+ return fmt.Errorf("invalid dns.resolvers[%d]: %q: expected host:port: %w", i, resolver, err)
47+ }
48+ }
49+ if c.PropagationTimeout <= 0 {
50+ return fmt.Errorf("dns.propagation_timeout must be positive")
51+ }
52+ if c.PollingInterval <= 0 {
53+ return fmt.Errorf("dns.polling_interval must be positive")
54+ }
55+ if c.PollingInterval >= c.PropagationTimeout {
56+ return fmt.Errorf("dns.polling_interval must be less than dns.propagation_timeout")
57+ }
58+ return nil
59+}
@@ -0,0 +1,59 @@
1+package publictls
2+
3+import (
4+ "fmt"
5+ "net"
6+ "strings"
7+ "time"
8+)
9+
10+const (
11+ DefaultDNSPropagationTimeout = 5 * time.Minute
12+ DefaultDNSPollingInterval = 5 * time.Second
13+)
14+
15+var DefaultDNSResolvers = []string{"1.1.1.1:53", "8.8.8.8:53"}
16+
17+// DNSConfig controls public DNS visibility checks used by ACME DNS-01.
18+// These are recursive resolvers, not authoritative DNS providers.
19+type DNSConfig struct {
20+ Resolvers []string
21+ PropagationTimeout time.Duration
22+ PollingInterval time.Duration
23+}
24+
25+// DefaultDNSConfig returns Gordon's default public DNS check configuration.
26+func DefaultDNSConfig() DNSConfig {
27+ resolvers := append([]string(nil), DefaultDNSResolvers...)
28+ return DNSConfig{
29+ Resolvers: resolvers,
30+ PropagationTimeout: DefaultDNSPropagationTimeout,
31+ PollingInterval: DefaultDNSPollingInterval,
32+ }
33+}
34+
35+// Validate checks DNSConfig for ACME DNS-01 usage.
36+func (c DNSConfig) Validate() error {
37+ if len(c.Resolvers) == 0 {
38+ return fmt.Errorf("dns.resolvers must contain at least one resolver")
39+ }
40+ for i, resolver := range c.Resolvers {
41+ resolver = strings.TrimSpace(resolver)
42+ if resolver == "" {
43+ return fmt.Errorf("invalid dns.resolvers[%d]: empty resolver", i)
44+ }
45+ if _, _, err := net.SplitHostPort(resolver); err != nil {
46+ return fmt.Errorf("invalid dns.resolvers[%d]: %q: expected host:port: %w", i, resolver, err)
47+ }
48+ }
49+ if c.PropagationTimeout <= 0 {
50+ return fmt.Errorf("dns.propagation_timeout must be positive")
51+ }
52+ if c.PollingInterval <= 0 {
53+ return fmt.Errorf("dns.polling_interval must be positive")
54+ }
55+ if c.PollingInterval >= c.PropagationTimeout {
56+ return fmt.Errorf("dns.polling_interval must be less than dns.propagation_timeout")
57+ }
58+ return nil
59+}
A internal/usecase/publictls/dns_config_test.go +91 -0
@@ -0,0 +1,91 @@
1+package publictls
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/stretchr/testify/assert"
8+ "github.com/stretchr/testify/require"
9+)
10+
11+func TestDefaultDNSConfig(t *testing.T) {
12+ cfg := DefaultDNSConfig()
13+
14+ assert.Equal(t, []string{"1.1.1.1:53", "8.8.8.8:53"}, cfg.Resolvers)
15+ assert.Equal(t, 5*time.Minute, cfg.PropagationTimeout)
16+ assert.Equal(t, 5*time.Second, cfg.PollingInterval)
17+}
18+
19+func TestDNSConfigValidate(t *testing.T) {
20+ tests := []struct {
21+ name string
22+ cfg DNSConfig
23+ wantErr string
24+ }{
25+ {
26+ name: "valid",
27+ cfg: DNSConfig{
28+ Resolvers: []string{"1.1.1.1:53", "8.8.8.8:53"},
29+ PropagationTimeout: 5 * time.Minute,
30+ PollingInterval: 5 * time.Second,
31+ },
32+ },
33+ {
34+ name: "empty resolvers",
35+ cfg: DNSConfig{
36+ Resolvers: nil,
37+ PropagationTimeout: 5 * time.Minute,
38+ PollingInterval: 5 * time.Second,
39+ },
40+ wantErr: "dns.resolvers must contain at least one resolver",
41+ },
42+ {
43+ name: "resolver missing port",
44+ cfg: DNSConfig{
45+ Resolvers: []string{"1.1.1.1"},
46+ PropagationTimeout: 5 * time.Minute,
47+ PollingInterval: 5 * time.Second,
48+ },
49+ wantErr: "invalid dns.resolvers[0]",
50+ },
51+ {
52+ name: "zero timeout",
53+ cfg: DNSConfig{
54+ Resolvers: []string{"1.1.1.1:53"},
55+ PropagationTimeout: 0,
56+ PollingInterval: 5 * time.Second,
57+ },
58+ wantErr: "dns.propagation_timeout must be positive",
59+ },
60+ {
61+ name: "zero polling interval",
62+ cfg: DNSConfig{
63+ Resolvers: []string{"1.1.1.1:53"},
64+ PropagationTimeout: 5 * time.Minute,
65+ PollingInterval: 0,
66+ },
67+ wantErr: "dns.polling_interval must be positive",
68+ },
69+ {
70+ name: "polling interval equals timeout",
71+ cfg: DNSConfig{
72+ Resolvers: []string{"1.1.1.1:53"},
73+ PropagationTimeout: 5 * time.Second,
74+ PollingInterval: 5 * time.Second,
75+ },
76+ wantErr: "dns.polling_interval must be less than dns.propagation_timeout",
77+ },
78+ }
79+
80+ for _, tt := range tests {
81+ t.Run(tt.name, func(t *testing.T) {
82+ err := tt.cfg.Validate()
83+ if tt.wantErr == "" {
84+ require.NoError(t, err)
85+ return
86+ }
87+ require.Error(t, err)
88+ assert.Contains(t, err.Error(), tt.wantErr)
89+ })
90+ }
91+}
@@ -0,0 +1,91 @@
1+package publictls
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/stretchr/testify/assert"
8+ "github.com/stretchr/testify/require"
9+)
10+
11+func TestDefaultDNSConfig(t *testing.T) {
12+ cfg := DefaultDNSConfig()
13+
14+ assert.Equal(t, []string{"1.1.1.1:53", "8.8.8.8:53"}, cfg.Resolvers)
15+ assert.Equal(t, 5*time.Minute, cfg.PropagationTimeout)
16+ assert.Equal(t, 5*time.Second, cfg.PollingInterval)
17+}
18+
19+func TestDNSConfigValidate(t *testing.T) {
20+ tests := []struct {
21+ name string
22+ cfg DNSConfig
23+ wantErr string
24+ }{
25+ {
26+ name: "valid",
27+ cfg: DNSConfig{
28+ Resolvers: []string{"1.1.1.1:53", "8.8.8.8:53"},
29+ PropagationTimeout: 5 * time.Minute,
30+ PollingInterval: 5 * time.Second,
31+ },
32+ },
33+ {
34+ name: "empty resolvers",
35+ cfg: DNSConfig{
36+ Resolvers: nil,
37+ PropagationTimeout: 5 * time.Minute,
38+ PollingInterval: 5 * time.Second,
39+ },
40+ wantErr: "dns.resolvers must contain at least one resolver",
41+ },
42+ {
43+ name: "resolver missing port",
44+ cfg: DNSConfig{
45+ Resolvers: []string{"1.1.1.1"},
46+ PropagationTimeout: 5 * time.Minute,
47+ PollingInterval: 5 * time.Second,
48+ },
49+ wantErr: "invalid dns.resolvers[0]",
50+ },
51+ {
52+ name: "zero timeout",
53+ cfg: DNSConfig{
54+ Resolvers: []string{"1.1.1.1:53"},
55+ PropagationTimeout: 0,
56+ PollingInterval: 5 * time.Second,
57+ },
58+ wantErr: "dns.propagation_timeout must be positive",
59+ },
60+ {
61+ name: "zero polling interval",
62+ cfg: DNSConfig{
63+ Resolvers: []string{"1.1.1.1:53"},
64+ PropagationTimeout: 5 * time.Minute,
65+ PollingInterval: 0,
66+ },
67+ wantErr: "dns.polling_interval must be positive",
68+ },
69+ {
70+ name: "polling interval equals timeout",
71+ cfg: DNSConfig{
72+ Resolvers: []string{"1.1.1.1:53"},
73+ PropagationTimeout: 5 * time.Second,
74+ PollingInterval: 5 * time.Second,
75+ },
76+ wantErr: "dns.polling_interval must be less than dns.propagation_timeout",
77+ },
78+ }
79+
80+ for _, tt := range tests {
81+ t.Run(tt.name, func(t *testing.T) {
82+ err := tt.cfg.Validate()
83+ if tt.wantErr == "" {
84+ require.NoError(t, err)
85+ return
86+ }
87+ require.Error(t, err)
88+ assert.Contains(t, err.Error(), tt.wantErr)
89+ })
90+ }
91+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close