From ec73f9ecd88a4cdd5570bb422b93998174f7c4d9 Mon Sep 17 00:00:00 2001 From: Pasha Sviderski Date: Tue, 12 Aug 2025 16:40:21 +1000 Subject: [PATCH] chore: handle x-caddy: path/to/Caddyfile to read Caddy config in compose from file --- pkg/client/compose/caddy.go | 49 ++++++++++++++++++++ pkg/client/compose/caddy_test.go | 65 ++++++++++++++++++++++----- pkg/client/compose/project.go | 5 ++- pkg/client/compose/service_test.go | 5 ++- pkg/client/compose/testdata/Caddyfile | 3 ++ 5 files changed, 115 insertions(+), 12 deletions(-) create mode 100644 pkg/client/compose/testdata/Caddyfile diff --git a/pkg/client/compose/caddy.go b/pkg/client/compose/caddy.go index 5f82e527..28c7aa7a 100644 --- a/pkg/client/compose/caddy.go +++ b/pkg/client/compose/caddy.go @@ -2,7 +2,11 @@ package compose import ( "fmt" + "os" + "path/filepath" + "strings" + "github.com/compose-spec/compose-go/v2/types" "github.com/mitchellh/mapstructure" ) @@ -41,3 +45,48 @@ func (c *Caddy) DecodeMapstructure(value any) error { } return nil } + +// isCaddyfilePath determines if a string is likely a file path rather than inline Caddyfile config. +func isCaddyfilePath(s string) bool { + s = strings.TrimSpace(s) + if s == "" { + return false + } + + // For simplicity, multi-line string is considered an inline Caddyfile content. + return !strings.Contains(s, "\n") +} + +// transformServicesCaddyExtension processes Caddy extensions to load configs from files if needed. +func transformServicesCaddyExtension(project *types.Project) (*types.Project, error) { + return project.WithServicesTransform(func(name string, service types.ServiceConfig) (types.ServiceConfig, error) { + ext, ok := service.Extensions[CaddyExtensionKey] + if !ok { + return service, nil + } + + caddy, ok := ext.(Caddy) + if !ok { + return service, nil + } + + // Load the Caddyfile config from file if it's a path and replace the path with its content. + if isCaddyfilePath(caddy.Config) { + configPath := caddy.Config + if !filepath.IsAbs(configPath) { + configPath = filepath.Join(project.WorkingDir, configPath) + } + + content, err := os.ReadFile(configPath) + if err != nil { + return service, fmt.Errorf("read Caddy config (Caddyfile) from file '%s' for service '%s': %w", + caddy.Config, name, err) + } + + caddy.Config = string(content) + service.Extensions[CaddyExtensionKey] = caddy + } + + return service, nil + }) +} diff --git a/pkg/client/compose/caddy_test.go b/pkg/client/compose/caddy_test.go index 46225d55..acf59c72 100644 --- a/pkg/client/compose/caddy_test.go +++ b/pkg/client/compose/caddy_test.go @@ -9,10 +9,10 @@ import ( func TestCaddyExtension(t *testing.T) { tests := []struct { - name string - composeYAML string - expectedConfig string - wantErr bool + name string + composeYAML string + wantConfig string + wantErr bool }{ { name: "x-caddy as string", @@ -25,7 +25,7 @@ services: reverse_proxy web:80 } `, - expectedConfig: `example.com { + wantConfig: `example.com { reverse_proxy web:80 } `, @@ -42,12 +42,24 @@ services: reverse_proxy web:80 } `, - expectedConfig: `example.com { + wantConfig: `example.com { reverse_proxy web:80 } `, }, - + { + name: "x-caddy with path to Caddyfile", + composeYAML: ` +services: + web: + image: nginx + x-caddy: testdata/Caddyfile +`, + wantConfig: `test.example.com { + reverse_proxy test:8000 +} +`, + }, { name: "x-caddy with empty object", composeYAML: ` @@ -56,7 +68,7 @@ services: image: nginx x-caddy: {} `, - expectedConfig: "", + wantConfig: "", }, { name: "x-caddy with empty string", @@ -66,7 +78,7 @@ services: image: nginx x-caddy: "" `, - expectedConfig: "", + wantConfig: "", }, { name: "x-caddy with extra unknown field should fail", @@ -117,7 +129,40 @@ services: caddy, ok := caddyExt.(Caddy) require.True(t, ok, "x-caddy extension is not Caddy type") - assert.Equal(t, tt.expectedConfig, caddy.Config) + assert.Equal(t, tt.wantConfig, caddy.Config) + }) + } +} + +func TestIsCaddyfilePath(t *testing.T) { + tests := []struct { + name string + input string + want bool + }{ + // Should be detected as file paths. + {"relative path with slash", "./Caddyfile", true}, + {"relative path parent", "../Caddyfile", true}, + {"relative path", "relative/path/to/file", true}, + {"absolute path", "/etc/caddy/Caddyfile", true}, + {"just Caddyfile", "Caddyfile", true}, + {"Caddyfile with suffix", "Caddyfile.app", true}, + {"caddyfile lowercase", "caddyfile", true}, + {"with .caddyfile extension", "my.caddyfile", true}, + {"with .Caddyfile extension", "my.Caddyfile", true}, + {"with .caddy extension", "config.caddy", true}, + {"with .conf extension", "caddy.conf", true}, + {"simple filename", "config", true}, + + // Should NOT be detected as file paths. + {"multiline config", "example.com {\n reverse_proxy :8080\n}", false}, + {"empty string", "", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isCaddyfilePath(tt.input) + assert.Equal(t, tt.want, result, "isCaddyfilePath(%q) should be %v", tt.input, tt.want) }) } } diff --git a/pkg/client/compose/project.go b/pkg/client/compose/project.go index 5db06cb9..fcbf4e7f 100644 --- a/pkg/client/compose/project.go +++ b/pkg/client/compose/project.go @@ -25,8 +25,8 @@ func LoadProject(ctx context.Context, paths []string, opts ...composecli.Project // If none was selected, get default Compose file names from current or parent folders. composecli.WithDefaultConfigPath, composecli.WithExtension(CaddyExtensionKey, Caddy{}), - composecli.WithExtension(PortsExtensionKey, PortsSource{}), composecli.WithExtension(MachinesExtensionKey, MachinesSource{}), + composecli.WithExtension(PortsExtensionKey, PortsSource{}), } options, err := composecli.NewProjectOptions( @@ -42,6 +42,9 @@ func LoadProject(ctx context.Context, paths []string, opts ...composecli.Project return nil, err } + if project, err = transformServicesCaddyExtension(project); err != nil { + return nil, err + } if project, err = transformServicesPortsExtension(project); err != nil { return nil, err } diff --git a/pkg/client/compose/service_test.go b/pkg/client/compose/service_test.go index 689f6999..b591303e 100644 --- a/pkg/client/compose/service_test.go +++ b/pkg/client/compose/service_test.go @@ -46,7 +46,10 @@ func loadProjectFromContent(t *testing.T, content string) (*types.Project, error return nil, err } - // Apply ports extension transformation since we're not using LoadProject + // Apply extension transformations since we're not using LoadProject. + if project, err = transformServicesCaddyExtension(project); err != nil { + return nil, err + } if project, err = transformServicesPortsExtension(project); err != nil { return nil, err } diff --git a/pkg/client/compose/testdata/Caddyfile b/pkg/client/compose/testdata/Caddyfile new file mode 100644 index 00000000..cf9c89d9 --- /dev/null +++ b/pkg/client/compose/testdata/Caddyfile @@ -0,0 +1,3 @@ +test.example.com { + reverse_proxy test:8000 +}