Files
CaddyProxyManager/backend/internal/caddy/reload.go
Pacerino 7ff0949067 [Backend] Add Caddy provider abstraction with Caddyfile and API modes
Introduce a Provider interface selectable via CPM_CADDY_MODE:
- CaddyfileProvider renders per-host snippets (template whitespace fixed)
- APIProvider manages Caddy's JSON config via the admin API
Add configurable reload strategy (systemd/exec/api/none) and a
configurable listen address for the bootstrapped server in API mode.
Includes unit tests for the JSON builder, provider factory and the
API provider (httptest).
2026-06-14 02:34:54 +02:00

104 lines
2.6 KiB
Go

package caddy
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"strings"
"time"
"github.com/Pacerino/CaddyProxyManager/internal/config"
"github.com/Pacerino/CaddyProxyManager/internal/logger"
"github.com/coreos/go-systemd/v22/dbus"
)
// Reload triggers Caddy to pick up configuration changes using the strategy
// configured via CPM_CADDY_RELOADSTRATEGY.
func Reload() error {
switch strings.ToLower(config.Configuration.Caddy.ReloadStrategy) {
case "", "systemd":
return reloadSystemd()
case "exec":
return reloadExec()
case "api":
return reloadAPI()
case "none":
return nil
default:
return fmt.Errorf("unknown reload strategy %q (expected systemd, exec, api or none)", config.Configuration.Caddy.ReloadStrategy)
}
}
// reloadSystemd reloads the caddy unit over the systemd D-Bus interface.
func reloadSystemd() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
conn, err := dbus.NewSystemdConnectionContext(ctx)
if err != nil {
return err
}
defer conn.Close()
resChan := make(chan string)
_, err = conn.ReloadOrRestartUnitContext(ctx, config.Configuration.Caddy.Service, "replace", resChan)
if err != nil {
logger.Error("CaddyReloadError", err)
return err
}
<-resChan
return nil
}
// reloadExec runs `caddy reload --config <Caddyfile>` to apply changes.
func reloadExec() error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx,
config.Configuration.Caddy.Binary,
"reload",
"--config", config.Configuration.CaddyFile,
)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
logger.Error("CaddyReloadError", err)
return err
}
return nil
}
// reloadAPI loads the on-disk Caddyfile through the admin API's /load endpoint.
func reloadAPI() error {
data, err := os.ReadFile(config.Configuration.CaddyFile)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
url := strings.TrimRight(config.Configuration.Caddy.AdminURL, "/") + "/load"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(data))
if err != nil {
return err
}
req.Header.Set("Content-Type", "text/caddyfile")
resp, err := http.DefaultClient.Do(req)
if err != nil {
logger.Error("CaddyReloadError", err)
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("caddy /load returned %d: %s", resp.StatusCode, string(body))
}
return nil
}