Skip to content

Commit 97bdb8e

Browse files
committed
fix: skip validation for user-defined Caddy configs if caddy not running locally
1 parent 5baa808 commit 97bdb8e

File tree

5 files changed

+153
-49
lines changed

5 files changed

+153
-49
lines changed

internal/machine/caddyconfig/caddyfile.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,11 @@ func NewCaddyfileGenerator(machineID string, validator CaddyfileValidator, log *
9595
// [service-a x-caddy]
9696
// ...
9797
// [service-z x-caddy]
98-
func (g *CaddyfileGenerator) Generate(ctx context.Context, records []store.ContainerRecord) (string, error) {
98+
//
99+
// If includeCustom is false, custom Caddy configs (x-caddy) are not included in the generated Caddyfile.
100+
func (g *CaddyfileGenerator) Generate(
101+
ctx context.Context, records []store.ContainerRecord, includeCustom bool,
102+
) (string, error) {
99103
containers := make([]api.ServiceContainer, len(records))
100104
for i, cr := range records {
101105
containers[i] = cr.Container
@@ -113,6 +117,12 @@ func (g *CaddyfileGenerator) Generate(ctx context.Context, records []store.Conta
113117
return "", fmt.Errorf("generate base Caddyfile from service ports: %w", err)
114118
}
115119

120+
if !includeCustom {
121+
return fmt.Sprintf("%s\n%s\n"+
122+
"# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine.\n",
123+
caddyfileHeader, caddyfile), nil
124+
}
125+
116126
upstreams := serviceUpstreams(containers)
117127
// Track validation errors for reporting.
118128
var configErrors []string

internal/machine/caddyconfig/caddyfile_test.go

Lines changed: 109 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ import (
1818
"github.com/stretchr/testify/require"
1919
)
2020

21-
func TestCaddyfileGenerator(t *testing.T) {
22-
caddyfileHeader := `# This file is autogenerated by Uncloud based on the configuration of running services.
21+
const testCaddyfileHeader = `# This file is autogenerated by Uncloud based on the configuration of running services.
2322
# Do not edit manually. Any manual changes will be overwritten on the next update.
2423
2524
# Health check endpoint to verify Caddy reachability on this machine.
@@ -38,6 +37,7 @@ http:// {
3837
}
3938
`
4039

40+
func TestCaddyfileGenerator(t *testing.T) {
4141
tests := []struct {
4242
name string
4343
containers []store.ContainerRecord
@@ -47,14 +47,14 @@ http:// {
4747
{
4848
name: "empty containers",
4949
containers: []store.ContainerRecord{},
50-
want: caddyfileHeader,
50+
want: testCaddyfileHeader,
5151
},
5252
{
5353
name: "HTTP container",
5454
containers: []store.ContainerRecord{
5555
newContainerRecord(newContainer("10.210.0.2", "app.example.com:8080/http"), "mach1"),
5656
},
57-
want: caddyfileHeader + `
57+
want: testCaddyfileHeader + `
5858
# Sites generated from service ports.
5959
6060
http://app.example.com {
@@ -71,7 +71,7 @@ http://app.example.com {
7171
newContainerRecord(newContainer("10.210.0.2", "app.example.com:8080/http"), "mach1"),
7272
newContainerRecord(newContainer("10.210.0.3", "app.example.com:8080/http"), "mach1"),
7373
},
74-
want: caddyfileHeader + `
74+
want: testCaddyfileHeader + `
7575
# Sites generated from service ports.
7676
7777
http://app.example.com {
@@ -87,7 +87,7 @@ http://app.example.com {
8787
containers: []store.ContainerRecord{
8888
newContainerRecord(newContainer("10.210.0.2", "secure.example.com:8000/https"), "mach1"),
8989
},
90-
want: caddyfileHeader + `
90+
want: testCaddyfileHeader + `
9191
# Sites generated from service ports.
9292
9393
https://secure.example.com {
@@ -127,7 +127,7 @@ https://secure.example.com {
127127
"mach1",
128128
),
129129
},
130-
want: caddyfileHeader + `
130+
want: testCaddyfileHeader + `
131131
# Sites generated from service ports.
132132
133133
http://app.example.com {
@@ -157,14 +157,14 @@ https://secure.example.com {
157157
containers: []store.ContainerRecord{
158158
newContainerRecord(newContainerWithoutNetwork("ignored.example.com:8080/http"), "mach1"),
159159
},
160-
want: caddyfileHeader,
160+
want: testCaddyfileHeader,
161161
},
162162
{
163163
name: "container with invalid port ignored",
164164
containers: []store.ContainerRecord{
165165
newContainerRecord(newContainer("10.210.0.2", "invalid-port"), "mach1"),
166166
},
167-
want: caddyfileHeader,
167+
want: testCaddyfileHeader,
168168
},
169169
{
170170
name: "containers with unsupported protocols and host mode ignored",
@@ -173,7 +173,7 @@ https://secure.example.com {
173173
newContainerRecord(newContainer("10.210.0.3", "5000/udp"), "mach1"),
174174
newContainerRecord(newContainer("10.210.0.4", "80:8080/tcp@host"), "mach1"),
175175
},
176-
want: caddyfileHeader,
176+
want: testCaddyfileHeader,
177177
},
178178
}
179179

@@ -183,7 +183,7 @@ https://secure.example.com {
183183
// Validator is not expected to be called in these tests.
184184
generator := NewCaddyfileGenerator("test-machine-id", nil, nil)
185185

186-
config, err := generator.Generate(ctx, tt.containers)
186+
config, err := generator.Generate(ctx, tt.containers, true)
187187

188188
if tt.wantErr {
189189
assert.Error(t, err)
@@ -197,25 +197,6 @@ https://secure.example.com {
197197
}
198198

199199
func TestCaddyfileGeneratorWithCustomConfigs(t *testing.T) {
200-
caddyfileBase := `# This file is autogenerated by Uncloud based on the configuration of running services.
201-
# Do not edit manually. Any manual changes will be overwritten on the next update.
202-
203-
# Health check endpoint to verify Caddy reachability on this machine.
204-
http:// {
205-
handle /.uncloud-verify {
206-
respond "test-machine-id" 200
207-
}
208-
log
209-
}
210-
211-
(common_proxy) {
212-
# Retry failed requests up to lb_retries times against other available upstreams.
213-
lb_retries 3
214-
# Upstreams are marked unhealthy for fail_duration after a failed request (passive health checking).
215-
fail_duration 30s
216-
}
217-
`
218-
219200
tests := []struct {
220201
name string
221202
containers []store.ContainerRecord
@@ -275,7 +256,7 @@ web.example.com {
275256
time.Now(),
276257
),
277258
},
278-
want: caddyfileBase + `
259+
want: testCaddyfileHeader + `
279260
# User-defined config for service 'web'.
280261
# Custom config for web service
281262
web.example.com {
@@ -297,7 +278,7 @@ bad.config.com {
297278
time.Now(),
298279
),
299280
},
300-
want: caddyfileBase + `
281+
want: testCaddyfileHeader + `
301282
# Skipped invalid user-defined configs:
302283
# - service 'bad-service': validation failed: invalid config detected
303284
`,
@@ -316,7 +297,7 @@ bad.template.com {
316297
time.Now(),
317298
),
318299
},
319-
want: caddyfileBase + `
300+
want: testCaddyfileHeader + `
320301
# Skipped invalid user-defined configs:
321302
# - service 'bad-template': failed to render template: parse config as Go template: template: Caddyfile:3: unexpected "}" in operand
322303
`,
@@ -335,7 +316,7 @@ localhost {
335316
time.Now(),
336317
),
337318
},
338-
want: caddyfileBase + `
319+
want: testCaddyfileHeader + `
339320
# Skipped invalid user-defined configs:
340321
# - service 'caddy': validation failed: invalid config detected
341322
`,
@@ -354,7 +335,7 @@ localhost {
354335
time.Now(),
355336
),
356337
},
357-
want: caddyfileBase,
338+
want: testCaddyfileHeader,
358339
},
359340
{
360341
name: "multiple services with mixed valid and invalid configs",
@@ -388,7 +369,7 @@ bad.example.com {
388369
time.Now(),
389370
),
390371
},
391-
want: caddyfileBase + `
372+
want: testCaddyfileHeader + `
392373
# User-defined config for service 'api'.
393374
api.example.com {
394375
reverse_proxy api:8080
@@ -490,7 +471,7 @@ api.example.com {
490471
"test-machine-id",
491472
),
492473
},
493-
want: caddyfileBase + `
474+
want: testCaddyfileHeader + `
494475
# Sites generated from service ports.
495476
496477
http://api.example.com {
@@ -530,7 +511,7 @@ new.example.com {
530511
time.Now(),
531512
),
532513
},
533-
want: caddyfileBase + `
514+
want: testCaddyfileHeader + `
534515
# User-defined config for service 'web'.
535516
# New config
536517
new.example.com {
@@ -815,7 +796,7 @@ invalid.example.com {
815796
time.Now(),
816797
),
817798
},
818-
want: caddyfileBase + `
799+
want: testCaddyfileHeader + `
819800
# User-defined config for service 'valid'.
820801
valid.example.com {
821802
respond "Valid config"
@@ -843,7 +824,7 @@ valid.example.com {
843824
t.Run(tt.name, func(t *testing.T) {
844825
generator := NewCaddyfileGenerator("test-machine-id", validator, nil)
845826

846-
config, err := generator.Generate(ctx, tt.containers)
827+
config, err := generator.Generate(ctx, tt.containers, true)
847828

848829
if tt.wantErr {
849830
assert.Error(t, err)
@@ -899,6 +880,94 @@ func newContainerRecordWithCaddyConfig(serviceName, ip, caddyConfig, machineID s
899880
}
900881
}
901882

883+
func TestCaddyfileGeneratorWithoutCustomConfigs(t *testing.T) {
884+
// Test that when includeCustom is false (Caddy not available), x-caddy configs are skipped.
885+
tests := []struct {
886+
name string
887+
containers []store.ContainerRecord
888+
want string
889+
}{
890+
{
891+
name: "x-caddy configs are skipped",
892+
containers: []store.ContainerRecord{
893+
newContainerRecordWithCaddyConfig(
894+
"caddy",
895+
"10.210.0.1",
896+
`# Global config
897+
{
898+
global directive
899+
}`,
900+
"test-machine-id",
901+
time.Now(),
902+
),
903+
newContainerRecordWithCaddyConfig(
904+
"web",
905+
"10.210.0.2",
906+
`web.example.com {
907+
reverse_proxy web:3000
908+
}`,
909+
"test-machine-id",
910+
time.Now(),
911+
),
912+
newContainerRecordWithPorts(
913+
"api",
914+
"10.210.0.3",
915+
[]string{"api.example.com:8080/http"},
916+
"test-machine-id",
917+
),
918+
},
919+
want: testCaddyfileHeader + `
920+
# Sites generated from service ports.
921+
922+
http://api.example.com {
923+
reverse_proxy 10.210.0.3:8080 {
924+
import common_proxy
925+
}
926+
log
927+
}
928+
929+
# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine.
930+
`,
931+
},
932+
{
933+
name: "no containers with x-caddy configs",
934+
containers: []store.ContainerRecord{
935+
newContainerRecordWithPorts(
936+
"api",
937+
"10.210.0.3",
938+
[]string{"api.example.com:8080/http"},
939+
"test-machine-id",
940+
),
941+
},
942+
want: testCaddyfileHeader + `
943+
# Sites generated from service ports.
944+
945+
http://api.example.com {
946+
reverse_proxy 10.210.0.3:8080 {
947+
import common_proxy
948+
}
949+
log
950+
}
951+
952+
# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine.
953+
`,
954+
},
955+
}
956+
957+
ctx := context.Background()
958+
for _, tt := range tests {
959+
t.Run(tt.name, func(t *testing.T) {
960+
// Validator is not expected to be called in these tests.
961+
generator := NewCaddyfileGenerator("test-machine-id", nil, nil)
962+
963+
config, err := generator.Generate(ctx, tt.containers, false)
964+
require.NoError(t, err)
965+
966+
assert.Equal(t, tt.want, config, "Generated Caddyfile doesn't match")
967+
})
968+
}
969+
}
970+
902971
func newContainerRecordWithPorts(serviceName, ip string, ports []string, machineID string) store.ContainerRecord {
903972
portsLabel := strings.Join(ports, ",")
904973
return store.ContainerRecord{

internal/machine/caddyconfig/client.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,24 @@ func NewCaddyAdminClient(socketPath string) *CaddyAdminClient {
3434
}
3535
}
3636

37+
// IsAvailable checks if the local Caddy instance is running and responding to admin API requests.
38+
func (c *CaddyAdminClient) IsAvailable(ctx context.Context) bool {
39+
// Caddy doesn't serve a /ping endpoint. It's a random endpoint we can use to check if Caddy is running.
40+
req, err := http.NewRequestWithContext(ctx, "GET", "http://localhost/ping", nil)
41+
if err != nil {
42+
return false
43+
}
44+
45+
resp, err := c.client.Do(req)
46+
if err != nil {
47+
return false
48+
}
49+
defer resp.Body.Close()
50+
51+
// Any HTTP response means Caddy is running and accessible.
52+
return true
53+
}
54+
3755
// Adapt converts a Caddyfile to JSON configuration without loading or running it.
3856
func (c *CaddyAdminClient) Adapt(ctx context.Context, caddyfile string) (string, error) {
3957
req, err := http.NewRequestWithContext(ctx, "POST", "http://localhost/adapt", strings.NewReader(caddyfile))

internal/machine/caddyconfig/controller.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,20 @@ func filterHealthyContainers(containers []store.ContainerRecord) []store.Contain
109109
}
110110

111111
func (c *Controller) generateAndLoadCaddyfile(ctx context.Context, containers []store.ContainerRecord) {
112-
caddyfile, err := c.generateCaddyfile(ctx, containers)
112+
// Check if Caddy is available before attempting to generate and load config.
113+
caddyAvailable := c.client.IsAvailable(ctx)
114+
115+
caddyfile, err := c.generateCaddyfile(ctx, containers, caddyAvailable)
113116
if err != nil {
114117
c.log.Error("Failed to generate Caddyfile configuration.", "err", err)
115118
return
116119
}
117120

121+
if !caddyAvailable {
122+
c.log.Debug("Caddy is not running on this machine, skipping configuration load.", "path", c.caddyfilePath)
123+
return
124+
}
125+
118126
if err = c.client.Load(ctx, caddyfile); err != nil {
119127
c.log.Error("Failed to load new Caddy configuration into local Caddy instance.",
120128
"err", err, "path", c.caddyfilePath)
@@ -123,8 +131,10 @@ func (c *Controller) generateAndLoadCaddyfile(ctx context.Context, containers []
123131
}
124132
}
125133

126-
func (c *Controller) generateCaddyfile(ctx context.Context, containers []store.ContainerRecord) (string, error) {
127-
caddyfile, err := c.generator.Generate(ctx, containers)
134+
func (c *Controller) generateCaddyfile(
135+
ctx context.Context, containers []store.ContainerRecord, caddyAvailable bool,
136+
) (string, error) {
137+
caddyfile, err := c.generator.Generate(ctx, containers, caddyAvailable)
128138
if err != nil {
129139
return "", fmt.Errorf("generate Caddyfile: %w", err)
130140
}

0 commit comments

Comments
 (0)