mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
chore: refactor Caddyfile generator to accept a validator
This commit is contained in:
@@ -45,7 +45,43 @@ https://{{$hostname}} {
|
|||||||
}{{end}}
|
}{{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)
|
httpHostUpstreams, httpsHostUpstreams := httpUpstreamsFromContainers(containers)
|
||||||
|
|
||||||
funcs := template.FuncMap{"join": strings.Join}
|
funcs := template.FuncMap{"join": strings.Join}
|
||||||
@@ -61,7 +97,7 @@ func GenerateCaddyfile(containers []api.ServiceContainer, verifyResponse string)
|
|||||||
HTTPSHostUpstreams map[string][]string
|
HTTPSHostUpstreams map[string][]string
|
||||||
}{
|
}{
|
||||||
VerifyPath: VerifyPath,
|
VerifyPath: VerifyPath,
|
||||||
VerifyResponse: verifyResponse,
|
VerifyResponse: g.MachineID,
|
||||||
HTTPHostUpstreams: httpHostUpstreams,
|
HTTPHostUpstreams: httpHostUpstreams,
|
||||||
HTTPSHostUpstreams: httpsHostUpstreams,
|
HTTPSHostUpstreams: httpsHostUpstreams,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGenerateCaddyfile(t *testing.T) {
|
func TestCaddyfileGenerator(t *testing.T) {
|
||||||
caddyfileHeader := `http:// {
|
caddyfileHeader := `http:// {
|
||||||
handle /.uncloud-verify {
|
handle /.uncloud-verify {
|
||||||
respond "verification-response-body" 200
|
respond "test-machine-id" 200
|
||||||
}
|
}
|
||||||
log
|
log
|
||||||
}
|
}
|
||||||
@@ -24,6 +24,10 @@ func TestGenerateCaddyfile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
generator := &CaddyfileGenerator{
|
||||||
|
MachineID: "test-machine-id",
|
||||||
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
containers []api.ServiceContainer
|
containers []api.ServiceContainer
|
||||||
@@ -182,7 +186,7 @@ http://app.example.com {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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 {
|
if tt.wantErr {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|||||||
@@ -22,13 +22,14 @@ const (
|
|||||||
// proxy. The generated configuration allows Caddy to route external traffic to service containers across the internal
|
// proxy. The generated configuration allows Caddy to route external traffic to service containers across the internal
|
||||||
// network.
|
// network.
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
store *store.Store
|
machineID string
|
||||||
configDir string
|
configDir string
|
||||||
verifyResponse string
|
generator *CaddyfileGenerator
|
||||||
log *slog.Logger
|
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 {
|
if err := os.MkdirAll(configDir, 0o750); err != nil {
|
||||||
return nil, fmt.Errorf("create directory for Caddy configuration '%s': %w", configDir, err)
|
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)
|
return nil, fmt.Errorf("change owner of directory for Caddy configuration '%s': %w", configDir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generator := &CaddyfileGenerator{
|
||||||
|
MachineID: machineID,
|
||||||
|
}
|
||||||
|
|
||||||
return &Controller{
|
return &Controller{
|
||||||
store: store,
|
machineID: machineID,
|
||||||
configDir: configDir,
|
configDir: configDir,
|
||||||
verifyResponse: verifyResponse,
|
generator: generator,
|
||||||
log: slog.With("component", "caddy-controller"),
|
store: store,
|
||||||
|
log: slog.With("component", "caddy-controller"),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,20 +79,20 @@ func (c *Controller) Run(ctx context.Context) error {
|
|||||||
|
|
||||||
containerRecords, err = c.store.ListContainers(ctx, store.ListOptions{})
|
containerRecords, err = c.store.ListContainers(ctx, store.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.log.Info("Failed to list containers.", "err", err)
|
c.log.Error("Failed to list containers.", "err", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
containers, err = c.filterAvailableContainers(containerRecords)
|
containers, err = c.filterAvailableContainers(containerRecords)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.log.Info("Failed to filter available containers.", "err", err)
|
c.log.Error("Failed to filter available containers.", "err", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = c.generateCaddyfile(containers); err != nil {
|
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 {
|
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)
|
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 {
|
func (c *Controller) generateCaddyfile(containers []api.ServiceContainer) error {
|
||||||
caddyfile, err := GenerateCaddyfile(containers, c.verifyResponse)
|
caddyfile, err := c.generator.Generate(containers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("generate Caddyfile: %w", err)
|
return fmt.Errorf("generate Caddyfile: %w", err)
|
||||||
}
|
}
|
||||||
caddyfilePath := filepath.Join(c.configDir, "Caddyfile")
|
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 {
|
if err = os.WriteFile(caddyfilePath, []byte(caddyfile), 0o640); err != nil {
|
||||||
return fmt.Errorf("write Caddyfile to file '%s': %w", caddyfilePath, err)
|
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 {
|
func (c *Controller) generateJSONConfig(containers []api.ServiceContainer) error {
|
||||||
config, err := GenerateJSONConfig(containers, c.verifyResponse)
|
config, err := GenerateJSONConfig(containers, c.machineID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ const (
|
|||||||
DefaultMachineSockPath = "/run/uncloud/machine.sock"
|
DefaultMachineSockPath = "/run/uncloud/machine.sock"
|
||||||
DefaultUncloudSockPath = "/run/uncloud/uncloud.sock"
|
DefaultUncloudSockPath = "/run/uncloud/uncloud.sock"
|
||||||
DefaultSockGroup = "uncloud"
|
DefaultSockGroup = "uncloud"
|
||||||
|
// DefaultCaddyAdminSockPath is the default path to the Caddy admin socket for validating the generated Caddy
|
||||||
|
// reverse proxy configuration.
|
||||||
|
DefaultCaddyAdminSockPath = "/run/uncloud/caddy/admin.sock"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -387,7 +390,12 @@ func (m *Machine) Run(ctx context.Context) error {
|
|||||||
|
|
||||||
// Create a new caddyconfig controller for managing the Caddy reverse proxy configuration.
|
// 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.
|
// It will also serve the current machine ID at /.uncloud-verify to verify Caddy reachability.
|
||||||
caddyconfigCtrl, err := caddyconfig.NewController(m.store, m.config.CaddyConfigDir, m.state.ID)
|
caddyconfigCtrl, err := caddyconfig.NewController(
|
||||||
|
m.state.ID,
|
||||||
|
m.config.CaddyConfigDir,
|
||||||
|
DefaultCaddyAdminSockPath,
|
||||||
|
m.store,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create caddyconfig controller: %w", err)
|
return fmt.Errorf("create caddyconfig controller: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user