Skip to content
183 changes: 183 additions & 0 deletions caddyconfig/configbuilder/configbuilder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package configbuilder

import (
"fmt"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
)

// Builder provides methods for safely building a Caddy configuration
// by accumulating apps, admin config, logging, storage, etc.
// It prevents common errors like duplicate app names.
type Builder struct {
config *caddy.Config
warnings *[]caddyconfig.Warning
// apps stores structured app configs before marshaling
// Multiple blocks can contribute to the same app
apps map[string]any
}

// New creates a new config builder with an empty configuration.
func New() *Builder {
warnings := []caddyconfig.Warning{}
return &Builder{
config: &caddy.Config{
AppsRaw: make(caddy.ModuleMap),
},
warnings: &warnings,
apps: make(map[string]any),
}
}

// Config returns the built configuration.
// This automatically finalizes any structured apps that haven't been marshaled yet.
func (b *Builder) Config() *caddy.Config {
b.finalize()
return b.config
}

// Warnings returns the accumulated warnings.
func (b *Builder) Warnings() []caddyconfig.Warning {
return *b.warnings
}

// AddWarning adds a warning to the builder.
func (b *Builder) AddWarning(message string) {
*b.warnings = append(*b.warnings, caddyconfig.Warning{
Message: message,
})
}

// GetApp retrieves a structured app config by name.
// Returns a pointer to the app and true if found, nil and false otherwise.
// The returned value can be type-asserted to the specific app type.
// Blocks should use this to get an existing app before modifying it.
func (b *Builder) GetApp(name string) (any, bool) {
app, ok := b.apps[name]
return app, ok
}

// GetTypedApp retrieves a structured app config by name with the correct type.
// Returns a pointer to the app and true if found, nil and false otherwise.
// This is a type-safe alternative to GetApp that uses generics.
// Example: httpApp, ok := builder.GetTypedApp[caddyhttp.App]("http")
func GetTypedApp[T any](b *Builder, name string) (*T, bool) {
app, ok := b.apps[name]
if !ok {
return nil, false
}
typedApp, ok := app.(*T)
return typedApp, ok
}

// CreateApp stores a new structured app config by name.
// Returns an error if an app with this name already exists.
// Blocks that want to modify an existing app should use GetApp() first.
func (b *Builder) CreateApp(name string, app any) error {
if _, exists := b.apps[name]; exists {
return fmt.Errorf("app '%s' already exists", name)
}
b.apps[name] = app
return nil
}

// finalize marshals all structured apps to JSON and adds them to the config.
// This is called automatically by Config().
func (b *Builder) finalize() {
for name, app := range b.apps {
b.addApp(name, app)
}
// Clear apps after finalizing so we don't re-add them if Config() is called again
b.apps = make(map[string]any)
}

// addApp is an internal method to add an app to the configuration by marshaling it to JSON.
// If an app with the same name already exists, a warning is added and the duplicate is ignored.
func (b *Builder) addApp(name string, val any) {
if b.config.AppsRaw == nil {
b.config.AppsRaw = make(caddy.ModuleMap)
}

if _, exists := b.config.AppsRaw[name]; exists {
b.AddWarning(fmt.Sprintf("app '%s' already exists in configuration, ignoring duplicate declaration", name))
return
}

b.config.AppsRaw[name] = caddyconfig.JSON(val, b.warnings)
}

// SetAdmin sets the admin configuration.
// If admin config already exists, it will be replaced.
func (b *Builder) SetAdmin(admin *caddy.AdminConfig) {
b.config.Admin = admin
}

// GetAdmin returns the admin configuration, creating it if it doesn't exist.
func (b *Builder) GetAdmin() *caddy.AdminConfig {
if b.config.Admin == nil {
b.config.Admin = new(caddy.AdminConfig)
}
return b.config.Admin
}

// SetStorage sets the storage configuration from a StorageConverter.
// If storage config already exists, it will be replaced.
func (b *Builder) SetStorage(storage caddy.StorageConverter) {
b.config.StorageRaw = caddyconfig.JSONModuleObject(
storage,
"module",
storage.(caddy.Module).CaddyModule().ID.Name(),
b.warnings,
)
}

// SetDefaultLogger sets the default logger configuration.
func (b *Builder) SetDefaultLogger(log *caddy.CustomLog) {
if b.config.Logging == nil {
b.config.Logging = &caddy.Logging{
Logs: make(map[string]*caddy.CustomLog),
}
}
if b.config.Logging.Logs == nil {
b.config.Logging.Logs = make(map[string]*caddy.CustomLog)
}
b.config.Logging.Logs[caddy.DefaultLoggerName] = log
}

// SetLogger sets a named logger configuration.
func (b *Builder) SetLogger(name string, log *caddy.CustomLog) {
if b.config.Logging == nil {
b.config.Logging = &caddy.Logging{
Logs: make(map[string]*caddy.CustomLog),
}
}
if b.config.Logging.Logs == nil {
b.config.Logging.Logs = make(map[string]*caddy.CustomLog)
}
b.config.Logging.Logs[name] = log
}

// AddRawApp adds a raw JSON app to the configuration.
// This is useful for adding apps that come from global options as pre-marshaled JSON.
func (b *Builder) AddRawApp(name string, rawJSON []byte) {
if b.config.AppsRaw == nil {
b.config.AppsRaw = make(caddy.ModuleMap)
}
b.config.AppsRaw[name] = rawJSON
}

Check failure on line 183 in caddyconfig/configbuilder/configbuilder.go

View workflow job for this annotation

GitHub Actions / lint (mac)

File is not properly formatted (gci)
28 changes: 14 additions & 14 deletions caddyconfig/httpcaddyfile/addresses.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ import (
// repetition may be undesirable, so call consolidateAddrMappings() to map
// multiple addresses to the same lists of server blocks (a many:many mapping).
// (Doing this is essentially a map-reduce technique.)
func (st *ServerType) mapAddressToProtocolToServerBlocks(originalServerBlocks []serverBlock,
func (st *ServerType) mapAddressToProtocolToServerBlocks(originalServerBlocks []ServerBlock,
options map[string]any,
) (map[string]map[string][]serverBlock, error) {
addrToProtocolToServerBlocks := map[string]map[string][]serverBlock{}
) (map[string]map[string][]ServerBlock, error) {
addrToProtocolToServerBlocks := map[string]map[string][]ServerBlock{}

type keyWithParsedKey struct {
key caddyfile.Token
Expand All @@ -94,7 +94,7 @@ func (st *ServerType) mapAddressToProtocolToServerBlocks(originalServerBlocks []
// key of a server block as its own, but without having to repeat its
// contents in cases where multiple keys really can be served together
addrToProtocolToKeyWithParsedKeys := map[string]map[string][]keyWithParsedKey{}
for j, key := range sblock.block.Keys {
for j, key := range sblock.Block.Keys {
parsedKey, err := ParseAddress(key.Text)
if err != nil {
return nil, fmt.Errorf("parsing key: %v", err)
Expand Down Expand Up @@ -154,7 +154,7 @@ func (st *ServerType) mapAddressToProtocolToServerBlocks(originalServerBlocks []

protocolToServerBlocks, ok := addrToProtocolToServerBlocks[addr]
if !ok {
protocolToServerBlocks = map[string][]serverBlock{}
protocolToServerBlocks = map[string][]ServerBlock{}
addrToProtocolToServerBlocks[addr] = protocolToServerBlocks
}

Expand All @@ -169,13 +169,13 @@ func (st *ServerType) mapAddressToProtocolToServerBlocks(originalServerBlocks []
parsedKeys[k] = keyWithParsedKey.parsedKey
}

protocolToServerBlocks[prot] = append(protocolToServerBlocks[prot], serverBlock{
block: caddyfile.ServerBlock{
protocolToServerBlocks[prot] = append(protocolToServerBlocks[prot], ServerBlock{
Block: caddyfile.ServerBlock{
Keys: keys,
Segments: sblock.block.Segments,
Segments: sblock.Block.Segments,
},
pile: sblock.pile,
parsedKeys: parsedKeys,
Pile: sblock.Pile,
ParsedKeys: parsedKeys,
})
}
}
Expand All @@ -192,7 +192,7 @@ func (st *ServerType) mapAddressToProtocolToServerBlocks(originalServerBlocks []
// Identical entries are deleted from the addrToServerBlocks map. Essentially, each pairing (each
// association from multiple addresses to multiple server blocks; i.e. each element of
// the returned slice) becomes a server definition in the output JSON.
func (st *ServerType) consolidateAddrMappings(addrToProtocolToServerBlocks map[string]map[string][]serverBlock) []sbAddrAssociation {
func (st *ServerType) consolidateAddrMappings(addrToProtocolToServerBlocks map[string]map[string][]ServerBlock) []sbAddrAssociation {
sbaddrs := make([]sbAddrAssociation, 0, len(addrToProtocolToServerBlocks))

addrs := make([]string, 0, len(addrToProtocolToServerBlocks))
Expand Down Expand Up @@ -267,7 +267,7 @@ func (st *ServerType) consolidateAddrMappings(addrToProtocolToServerBlocks map[s

// listenersForServerBlockAddress essentially converts the Caddyfile site addresses to a map from
// Caddy listener addresses and the protocols to serve them with to the parsed address for each server block.
func (st *ServerType) listenersForServerBlockAddress(sblock serverBlock, addr Address,
func (st *ServerType) listenersForServerBlockAddress(sblock ServerBlock, addr Address,
options map[string]any,
) (map[string]map[string]struct{}, error) {
switch addr.Scheme {
Expand Down Expand Up @@ -307,8 +307,8 @@ func (st *ServerType) listenersForServerBlockAddress(sblock serverBlock, addr Ad
}

// the bind directive specifies hosts (and potentially network), and the protocols to serve them with, but is optional
lnCfgVals := make([]addressesWithProtocols, 0, len(sblock.pile["bind"]))
for _, cfgVal := range sblock.pile["bind"] {
lnCfgVals := make([]addressesWithProtocols, 0, len(sblock.Pile["bind"]))
for _, cfgVal := range sblock.Pile["bind"] {
if val, ok := cfgVal.Value.(addressesWithProtocols); ok {
lnCfgVals = append(lnCfgVals, val)
}
Expand Down
127 changes: 127 additions & 0 deletions caddyconfig/httpcaddyfile/blocktypes/blocktypes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package blocktypes

import (
"fmt"
"sync"

"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/configbuilder"
)

// BlockTypeSetupFunc is a function that processes server blocks of a specific type
// and builds the configuration using the provided builder. It receives a config builder,
// the server blocks to process, and any global options.
type BlockTypeSetupFunc func(builder *configbuilder.Builder, blocks []caddyfile.ServerBlock, options map[string]any) ([]caddyconfig.Warning, error)

// BlockTypeInfo contains metadata about a registered block type.
type BlockTypeInfo struct {
SetupFunc BlockTypeSetupFunc
Parent string // Parent block type (e.g., "http" for "http.server")
}

var (
registeredBlockTypes = make(map[string]*BlockTypeInfo)
blockTypesMu sync.RWMutex
)

// RegisterBlockType registers a top-level block type handler with the given name.
// This allows the block type to be used in xcaddyfile configurations with [name] syntax.
// For example, RegisterBlockType("http", setupFunc) allows [http] blocks.
//
// Block type handlers are responsible for parsing their specific block types
// and mutating the provided caddy.Config accordingly.
func RegisterBlockType(name string, setupFunc BlockTypeSetupFunc) {
blockTypesMu.Lock()
defer blockTypesMu.Unlock()

if _, exists := registeredBlockTypes[name]; exists {
panic(fmt.Sprintf("block type %s already registered", name))
}

registeredBlockTypes[name] = &BlockTypeInfo{
SetupFunc: setupFunc,
Parent: "", // No parent for top-level block types
}
}

// RegisterChildBlockType registers a hierarchical block type under a parent.
// For example, RegisterChildBlockType("http.server", "http", setupFunc) registers
// [http.server] as a child of [http].
//
// Child blocks are processed after their parent and can access context set by the parent.
func RegisterChildBlockType(name, parent string, setupFunc BlockTypeSetupFunc) {
blockTypesMu.Lock()
defer blockTypesMu.Unlock()

if _, exists := registeredBlockTypes[name]; exists {
panic(fmt.Sprintf("block type %s already registered", name))
}

registeredBlockTypes[name] = &BlockTypeInfo{
SetupFunc: setupFunc,
Parent: parent,
}
}

// GetBlockType retrieves a registered block type handler by name.
// Returns the handler function and true if found, nil and false otherwise.
func GetBlockType(name string) (BlockTypeSetupFunc, bool) {
blockTypesMu.RLock()
defer blockTypesMu.RUnlock()

info, exists := registeredBlockTypes[name]
if !exists {
return nil, false
}
return info.SetupFunc, true
}

// GetBlockTypeInfo retrieves full block type info including parent relationship.
func GetBlockTypeInfo(name string) (*BlockTypeInfo, bool) {
blockTypesMu.RLock()
defer blockTypesMu.RUnlock()

info, exists := registeredBlockTypes[name]
return info, exists
}

// GetChildBlockTypes returns all child block types for a given parent.
func GetChildBlockTypes(parent string) []string {
blockTypesMu.RLock()
defer blockTypesMu.RUnlock()

var children []string
for name, info := range registeredBlockTypes {
if info.Parent == parent {
children = append(children, name)
}
}
return children
}

// RegisteredBlockTypes returns a list of all registered block type names.
func RegisteredBlockTypes() []string {
blockTypesMu.RLock()
defer blockTypesMu.RUnlock()

names := make([]string, 0, len(registeredBlockTypes))
for name := range registeredBlockTypes {
names = append(names, name)
}
return names
}
Loading
Loading