chore: refactor Caddyfile generator to accept a validator

This commit is contained in:
Pasha Sviderski
2025-08-18 18:48:32 +10:00
parent 455174ccb0
commit 93fef88fac
4 changed files with 76 additions and 21 deletions
+38 -2
View File
@@ -45,7 +45,43 @@ https://{{$hostname}} {
}{{end}}
`
func GenerateCaddyfile(containers []api.ServiceContainer, verifyResponse string) (string, error) {
// CaddyfileGenerator generates a Caddyfile configuration for the Caddy reverse proxy.
type CaddyfileGenerator struct {
// MachineID is the unique identifier of the machine where the controller is running.
MachineID string
Validator CaddyfileValidator
}
// CaddyfileValidator is an interface for validating Caddyfile configurations.
type CaddyfileValidator interface {
Validate(caddyfile string) error
}
// Generate creates a Caddyfile configuration based on the provided service containers.
// If a 'caddy' service container is running on this machine and defines a custom Caddy config (x-caddy) in its service
// spec, it will be validated and prepended to the generated Caddyfile. Custom Caddy configs (x-caddy) defined in other
// service specs are validated and appended to the generated Caddyfile. Invalid configs are logged and skipped to ensure
// the generated Caddyfile remains valid.
//
// The final Caddyfile structure includes:
//
// [caddy x-caddy]
// [generated Caddyfile from all service ports]
// [service-a x-caddy]
// ...
// [service-z x-caddy]
func (g *CaddyfileGenerator) Generate(containers []api.ServiceContainer) (string, error) {
baseCaddyfile, err := g.generateBaseFromPorts(containers)
if err != nil {
return "", fmt.Errorf("generate base Caddyfile from service ports: %w", err)
}
// TODO: Implement support for custom Caddy configs (x-caddy) in service specs.
return baseCaddyfile, nil
}
func (g *CaddyfileGenerator) generateBaseFromPorts(containers []api.ServiceContainer) (string, error) {
httpHostUpstreams, httpsHostUpstreams := httpUpstreamsFromContainers(containers)
funcs := template.FuncMap{"join": strings.Join}
@@ -61,7 +97,7 @@ func GenerateCaddyfile(containers []api.ServiceContainer, verifyResponse string)
HTTPSHostUpstreams map[string][]string
}{
VerifyPath: VerifyPath,
VerifyResponse: verifyResponse,
VerifyResponse: g.MachineID,
HTTPHostUpstreams: httpHostUpstreams,
HTTPSHostUpstreams: httpsHostUpstreams,
}
@@ -8,10 +8,10 @@ import (
"github.com/stretchr/testify/require"
)
func TestGenerateCaddyfile(t *testing.T) {
func TestCaddyfileGenerator(t *testing.T) {
caddyfileHeader := `http:// {
handle /.uncloud-verify {
respond "verification-response-body" 200
respond "test-machine-id" 200
}
log
}
@@ -24,6 +24,10 @@ func TestGenerateCaddyfile(t *testing.T) {
}
`
generator := &CaddyfileGenerator{
MachineID: "test-machine-id",
}
tests := []struct {
name string
containers []api.ServiceContainer
@@ -182,7 +186,7 @@ http://app.example.com {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config, err := GenerateCaddyfile(tt.containers, "verification-response-body")
config, err := generator.Generate(tt.containers)
if tt.wantErr {
assert.Error(t, err)
+22 -15
View File
@@ -22,13 +22,14 @@ const (
// proxy. The generated configuration allows Caddy to route external traffic to service containers across the internal
// network.
type Controller struct {
store *store.Store
configDir string
verifyResponse string
log *slog.Logger
machineID string
configDir string
generator *CaddyfileGenerator
store *store.Store
log *slog.Logger
}
func NewController(store *store.Store, configDir string, verifyResponse string) (*Controller, error) {
func NewController(machineID, configDir, adminSock string, store *store.Store) (*Controller, error) {
if err := os.MkdirAll(configDir, 0o750); err != nil {
return nil, fmt.Errorf("create directory for Caddy configuration '%s': %w", configDir, err)
}
@@ -36,11 +37,16 @@ func NewController(store *store.Store, configDir string, verifyResponse string)
return nil, fmt.Errorf("change owner of directory for Caddy configuration '%s': %w", configDir, err)
}
generator := &CaddyfileGenerator{
MachineID: machineID,
}
return &Controller{
store: store,
configDir: configDir,
verifyResponse: verifyResponse,
log: slog.With("component", "caddy-controller"),
machineID: machineID,
configDir: configDir,
generator: generator,
store: store,
log: slog.With("component", "caddy-controller"),
}, nil
}
@@ -73,20 +79,20 @@ func (c *Controller) Run(ctx context.Context) error {
containerRecords, err = c.store.ListContainers(ctx, store.ListOptions{})
if err != nil {
c.log.Info("Failed to list containers.", "err", err)
c.log.Error("Failed to list containers.", "err", err)
continue
}
containers, err = c.filterAvailableContainers(containerRecords)
if err != nil {
c.log.Info("Failed to filter available containers.", "err", err)
c.log.Error("Failed to filter available containers.", "err", err)
continue
}
if err = c.generateCaddyfile(containers); err != nil {
c.log.Info("Failed to generate Caddyfile configuration.", "err", err)
c.log.Error("Failed to generate Caddyfile configuration.", "err", err)
}
if err = c.generateJSONConfig(containers); err != nil {
c.log.Info("Failed to generate Caddy JSON configuration.", "err", err)
c.log.Error("Failed to generate Caddy JSON configuration.", "err", err)
}
c.log.Info("Updated Caddy configuration.", "dir", c.configDir)
@@ -110,12 +116,13 @@ func (c *Controller) filterAvailableContainers(
}
func (c *Controller) generateCaddyfile(containers []api.ServiceContainer) error {
caddyfile, err := GenerateCaddyfile(containers, c.verifyResponse)
caddyfile, err := c.generator.Generate(containers)
if err != nil {
return fmt.Errorf("generate Caddyfile: %w", err)
}
caddyfilePath := filepath.Join(c.configDir, "Caddyfile")
// TODO: use atomic file write to avoid partial loads on Caddy watch reload.
if err = os.WriteFile(caddyfilePath, []byte(caddyfile), 0o640); err != nil {
return fmt.Errorf("write Caddyfile to file '%s': %w", caddyfilePath, err)
}
@@ -127,7 +134,7 @@ func (c *Controller) generateCaddyfile(containers []api.ServiceContainer) error
}
func (c *Controller) generateJSONConfig(containers []api.ServiceContainer) error {
config, err := GenerateJSONConfig(containers, c.verifyResponse)
config, err := GenerateJSONConfig(containers, c.machineID)
if err != nil {
return err
}