diff --git a/internal/machine/caddyconfig/caddyfile.go b/internal/machine/caddyconfig/caddyfile.go new file mode 100644 index 00000000..c56bdcb7 --- /dev/null +++ b/internal/machine/caddyconfig/caddyfile.go @@ -0,0 +1,17 @@ +package caddyconfig + +import ( + "fmt" + + "github.com/psviderski/uncloud/pkg/api" +) + +func GenerateCaddyfile(containers []api.ServiceContainer, verifyResponse string) (string, error) { + return fmt.Sprintf(`http:// { + handle %s { + respond "%s" 200 + } + log +} +`, VerifyPath, verifyResponse), nil +} diff --git a/internal/machine/caddyconfig/controller.go b/internal/machine/caddyconfig/controller.go index 5055354e..49efbc57 100644 --- a/internal/machine/caddyconfig/controller.go +++ b/internal/machine/caddyconfig/controller.go @@ -23,23 +23,24 @@ const ( // network. type Controller struct { store *store.Store - path string + configDir string verifyResponse string + log *slog.Logger } -func NewController(store *store.Store, path string, verifyResponse string) (*Controller, error) { - dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0o750); err != nil { - return nil, fmt.Errorf("create parent directory for Caddy configuration '%s': %w", dir, err) +func NewController(store *store.Store, configDir string, verifyResponse string) (*Controller, error) { + if err := os.MkdirAll(configDir, 0o750); err != nil { + return nil, fmt.Errorf("create directory for Caddy configuration '%s': %w", configDir, err) } - if err := fs.Chown(dir, "", CaddyGroup); err != nil { - return nil, fmt.Errorf("change owner of parent directory for Caddy configuration '%s': %w", dir, err) + if err := fs.Chown(configDir, "", CaddyGroup); err != nil { + return nil, fmt.Errorf("change owner of directory for Caddy configuration '%s': %w", configDir, err) } return &Controller{ store: store, - path: path, + configDir: configDir, verifyResponse: verifyResponse, + log: slog.With("component", "caddy-controller"), }, nil } @@ -48,14 +49,18 @@ func (c *Controller) Run(ctx context.Context) error { if err != nil { return fmt.Errorf("subscribe to container changes: %w", err) } - slog.Info("Subscribed to container changes in the cluster to generate Caddy configuration.") + c.log.Info("Subscribed to container changes in the cluster to generate Caddy configuration.") containers, err := c.filterAvailableContainers(containerRecords) if err != nil { return fmt.Errorf("filter available containers: %w", err) } - if err = c.generateConfig(containers); err != nil { - return fmt.Errorf("generate Caddy configuration: %w", err) + + if err = c.generateCaddyfile(containers); err != nil { + return fmt.Errorf("generate Caddyfile configuration: %w", err) + } + if err = c.generateJSONConfig(containers); err != nil { + return fmt.Errorf("generate Caddy JSON configuration: %w", err) } for { @@ -64,23 +69,27 @@ func (c *Controller) Run(ctx context.Context) error { if !ok { return fmt.Errorf("containers subscription failed") } - slog.Debug("Cluster containers changed, updating Caddy configuration.") + c.log.Info("Cluster containers changed, updating Caddy configuration.") containerRecords, err = c.store.ListContainers(ctx, store.ListOptions{}) if err != nil { - slog.Error("Failed to list containers.", "err", err) + c.log.Info("Failed to list containers.", "err", err) continue } containers, err = c.filterAvailableContainers(containerRecords) if err != nil { - slog.Error("Failed to filter available containers.", "err", err) + c.log.Info("Failed to filter available containers.", "err", err) continue } - if err = c.generateConfig(containers); err != nil { - slog.Error("Failed to generate Caddy configuration.", "err", err) + + if err = c.generateCaddyfile(containers); err != nil { + c.log.Info("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) } - slog.Debug("Updated Caddy configuration.", "path", c.path) + c.log.Info("Updated Caddy configuration.", "dir", c.configDir) case <-ctx.Done(): return nil } @@ -100,8 +109,25 @@ func (c *Controller) filterAvailableContainers( return containers, nil } -func (c *Controller) generateConfig(containers []api.ServiceContainer) error { - config, err := GenerateConfig(containers, c.verifyResponse) +func (c *Controller) generateCaddyfile(containers []api.ServiceContainer) error { + caddyfile, err := GenerateCaddyfile(containers, c.verifyResponse) + if err != nil { + return fmt.Errorf("generate Caddyfile: %w", err) + } + caddyfilePath := filepath.Join(c.configDir, "Caddyfile") + + if err = os.WriteFile(caddyfilePath, []byte(caddyfile), 0o640); err != nil { + return fmt.Errorf("write Caddyfile to file '%s': %w", caddyfilePath, err) + } + if err = fs.Chown(caddyfilePath, "", CaddyGroup); err != nil { + return fmt.Errorf("change owner of Caddyfile '%s': %w", caddyfilePath, err) + } + + return nil +} + +func (c *Controller) generateJSONConfig(containers []api.ServiceContainer) error { + config, err := GenerateJSONConfig(containers, c.verifyResponse) if err != nil { return err } @@ -110,12 +136,13 @@ func (c *Controller) generateConfig(containers []api.ServiceContainer) error { if err != nil { return fmt.Errorf("marshal Caddy configuration: %w", err) } + configPath := filepath.Join(c.configDir, "caddy.json") - if err = os.WriteFile(c.path, configBytes, 0o640); err != nil { - return fmt.Errorf("write Caddy configuration to file '%s': %w", c.path, err) + if err = os.WriteFile(configPath, configBytes, 0o640); err != nil { + return fmt.Errorf("write Caddy configuration to file '%s': %w", configPath, err) } - if err = fs.Chown(c.path, "", CaddyGroup); err != nil { - return fmt.Errorf("change owner of Caddy configuration file '%s': %w", c.path, err) + if err = fs.Chown(configPath, "", CaddyGroup); err != nil { + return fmt.Errorf("change owner of Caddy configuration file '%s': %w", configPath, err) } return nil diff --git a/internal/machine/caddyconfig/config.go b/internal/machine/caddyconfig/jsonconfig.go similarity index 98% rename from internal/machine/caddyconfig/config.go rename to internal/machine/caddyconfig/jsonconfig.go index c6f6485a..00b8965d 100644 --- a/internal/machine/caddyconfig/config.go +++ b/internal/machine/caddyconfig/jsonconfig.go @@ -19,7 +19,7 @@ import ( "github.com/psviderski/uncloud/pkg/api" ) -func GenerateConfig(containers []api.ServiceContainer, verifyResponse string) (*caddy.Config, error) { +func GenerateJSONConfig(containers []api.ServiceContainer, verifyResponse string) (*caddy.Config, error) { // Maps hostnames to lists of upstreams (container IP:port pairs). httpHostUpstreams := make(map[string][]string) httpsHostUpstreams := make(map[string][]string) diff --git a/internal/machine/caddyconfig/config_test.go b/internal/machine/caddyconfig/jsonconfig_test.go similarity index 99% rename from internal/machine/caddyconfig/config_test.go rename to internal/machine/caddyconfig/jsonconfig_test.go index ebc139e4..aae03aa7 100644 --- a/internal/machine/caddyconfig/config_test.go +++ b/internal/machine/caddyconfig/jsonconfig_test.go @@ -378,7 +378,7 @@ func TestGenerateConfig(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - config, err := GenerateConfig(tt.containers, "verification-response-body") + config, err := GenerateJSONConfig(tt.containers, "verification-response-body") if tt.wantErr { assert.Error(t, err) diff --git a/internal/machine/machine.go b/internal/machine/machine.go index f47388ee..9896a943 100644 --- a/internal/machine/machine.go +++ b/internal/machine/machine.go @@ -61,9 +61,9 @@ type Config struct { // DockerClient manages system and user containers using the local Docker daemon. DockerClient *client.Client - // CaddyConfigPath specifies where the machine generates the Caddy reverse proxy configuration file for routing - // external traffic to service containers across the internal network. Default is DataDir/caddy/caddy.json. - CaddyConfigPath string + // CaddyConfigDir specifies the directory where the machine generates the Caddy reverse proxy configuration file + // for routing external traffic to service containers across the internal network. Default is DataDir/caddy. + CaddyConfigDir string // DNSUpstreams specifies the upstream DNS servers for the embedded internal DNS server. DNSUpstreams []netip.AddrPort } @@ -128,8 +128,8 @@ func (c *Config) SetDefaults() (*Config, error) { } } - if cfg.CaddyConfigPath == "" { - cfg.CaddyConfigPath = filepath.Join(cfg.DataDir, "caddy", "caddy.json") + if cfg.CaddyConfigDir == "" { + cfg.CaddyConfigDir = filepath.Join(cfg.DataDir, "caddy") } return &cfg, nil @@ -387,7 +387,7 @@ func (m *Machine) Run(ctx context.Context) error { // Create a new caddyconfig controller for managing the Caddy reverse proxy configuration. // It will also serve the current machine ID at /.uncloud-verify to verify Caddy reachability. - caddyconfigCtrl, err := caddyconfig.NewController(m.store, m.config.CaddyConfigPath, m.state.ID) + caddyconfigCtrl, err := caddyconfig.NewController(m.store, m.config.CaddyConfigDir, m.state.ID) if err != nil { return fmt.Errorf("create caddyconfig controller: %w", err) }