Merge branch 'master' into golangcilint-v2

This commit is contained in:
Mohammed Al Sahaf 2025-04-03 02:56:36 +04:00 committed by GitHub
commit 924273f6a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 172 additions and 123 deletions

104
caddy.go
View File

@ -81,13 +81,14 @@ type Config struct {
// associated value.
AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="`
apps map[string]App
storage certmagic.Storage
apps map[string]App
storage certmagic.Storage
eventEmitter eventEmitter
cancelFunc context.CancelFunc
// filesystems is a dict of filesystems that will later be loaded from and added to.
filesystems FileSystems
// fileSystems is a dict of fileSystems that will later be loaded from and added to.
fileSystems FileSystems
}
// App is a thing that Caddy runs.
@ -442,6 +443,10 @@ func run(newCfg *Config, start bool) (Context, error) {
}
globalMetrics.configSuccess.Set(1)
globalMetrics.configSuccessTime.SetToCurrentTime()
// TODO: This event is experimental and subject to change.
ctx.emitEvent("started", nil)
// now that the user's config is running, finish setting up anything else,
// such as remote admin endpoint, config loader, etc.
return ctx, finishSettingUp(ctx, ctx.cfg)
@ -509,7 +514,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
}
// create the new filesystem map
newCfg.filesystems = &filesystems.FilesystemMap{}
newCfg.fileSystems = &filesystems.FileSystemMap{}
// prepare the new config for use
newCfg.apps = make(map[string]App)
@ -696,6 +701,9 @@ func unsyncedStop(ctx Context) {
return
}
// TODO: This event is experimental and subject to change.
ctx.emitEvent("stopping", nil)
// stop each app
for name, a := range ctx.cfg.apps {
err := a.Stop()
@ -1038,6 +1046,92 @@ func Version() (simple, full string) {
return
}
// Event represents something that has happened or is happening.
// An Event value is not synchronized, so it should be copied if
// being used in goroutines.
//
// EXPERIMENTAL: Events are subject to change.
type Event struct {
// If non-nil, the event has been aborted, meaning
// propagation has stopped to other handlers and
// the code should stop what it was doing. Emitters
// may choose to use this as a signal to adjust their
// code path appropriately.
Aborted error
// The data associated with the event. Usually the
// original emitter will be the only one to set or
// change these values, but the field is exported
// so handlers can have full access if needed.
// However, this map is not synchronized, so
// handlers must not use this map directly in new
// goroutines; instead, copy the map to use it in a
// goroutine. Data may be nil.
Data map[string]any
id uuid.UUID
ts time.Time
name string
origin Module
}
// NewEvent creates a new event, but does not emit the event. To emit an
// event, call Emit() on the current instance of the caddyevents app insteaad.
//
// EXPERIMENTAL: Subject to change.
func NewEvent(ctx Context, name string, data map[string]any) (Event, error) {
id, err := uuid.NewRandom()
if err != nil {
return Event{}, fmt.Errorf("generating new event ID: %v", err)
}
name = strings.ToLower(name)
return Event{
Data: data,
id: id,
ts: time.Now(),
name: name,
origin: ctx.Module(),
}, nil
}
func (e Event) ID() uuid.UUID { return e.id }
func (e Event) Timestamp() time.Time { return e.ts }
func (e Event) Name() string { return e.name }
func (e Event) Origin() Module { return e.origin } // Returns the module that originated the event. May be nil, usually if caddy core emits the event.
// CloudEvent exports event e as a structure that, when
// serialized as JSON, is compatible with the
// CloudEvents spec.
func (e Event) CloudEvent() CloudEvent {
dataJSON, _ := json.Marshal(e.Data)
return CloudEvent{
ID: e.id.String(),
Source: e.origin.CaddyModule().String(),
SpecVersion: "1.0",
Type: e.name,
Time: e.ts,
DataContentType: "application/json",
Data: dataJSON,
}
}
// CloudEvent is a JSON-serializable structure that
// is compatible with the CloudEvents specification.
// See https://cloudevents.io.
// EXPERIMENTAL: Subject to change.
type CloudEvent struct {
ID string `json:"id"`
Source string `json:"source"`
SpecVersion string `json:"specversion"`
Type string `json:"type"`
Time time.Time `json:"time"`
DataContentType string `json:"datacontenttype,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
}
// ErrEventAborted cancels an event.
var ErrEventAborted = errors.New("event aborted")
// ActiveContext returns the currently-active context.
// This function is experimental and might be changed
// or removed in the future.

View File

@ -91,14 +91,14 @@ func (ctx *Context) OnCancel(f func()) {
ctx.cleanupFuncs = append(ctx.cleanupFuncs, f)
}
// Filesystems returns a ref to the FilesystemMap.
// FileSystems returns a ref to the FilesystemMap.
// EXPERIMENTAL: This API is subject to change.
func (ctx *Context) Filesystems() FileSystems {
func (ctx *Context) FileSystems() FileSystems {
// if no config is loaded, we use a default filesystemmap, which includes the osfs
if ctx.cfg == nil {
return &filesystems.FilesystemMap{}
return &filesystems.FileSystemMap{}
}
return ctx.cfg.filesystems
return ctx.cfg.fileSystems
}
// Returns the active metrics registry for the context
@ -277,6 +277,14 @@ func (ctx Context) LoadModule(structPointer any, fieldName string) (any, error)
return result, nil
}
// emitEvent is a small convenience method so the caddy core can emit events, if the event app is configured.
func (ctx Context) emitEvent(name string, data map[string]any) Event {
if ctx.cfg == nil || ctx.cfg.eventEmitter == nil {
return Event{}
}
return ctx.cfg.eventEmitter.Emit(ctx, name, data)
}
// loadModulesFromSomeMap loads modules from val, which must be a type of map[string]any.
// Depending on inlineModuleKey, it will be interpreted as either a ModuleMap (key is the module
// name) or as a regular map (key is not the module name, and module name is defined inline).
@ -429,6 +437,14 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
ctx.moduleInstances[id] = append(ctx.moduleInstances[id], val)
// if the loaded module happens to be an app that can emit events, store it so the
// core can have access to emit events without an import cycle
if ee, ok := val.(eventEmitter); ok {
if _, ok := ee.(App); ok {
ctx.cfg.eventEmitter = ee
}
}
return val, nil
}
@ -600,3 +616,11 @@ func (ctx *Context) WithValue(key, value any) Context {
exitFuncs: ctx.exitFuncs,
}
}
// eventEmitter is a small interface that inverts dependencies for
// the caddyevents package, so the core can emit events without an
// import cycle (i.e. the caddy package doesn't have to import
// the caddyevents package, which imports the caddy package).
type eventEmitter interface {
Emit(ctx Context, eventName string, data map[string]any) Event
}

View File

@ -7,10 +7,10 @@ import (
)
const (
DefaultFilesystemKey = "default"
DefaultFileSystemKey = "default"
)
var DefaultFilesystem = &wrapperFs{key: DefaultFilesystemKey, FS: OsFS{}}
var DefaultFileSystem = &wrapperFs{key: DefaultFileSystemKey, FS: OsFS{}}
// wrapperFs exists so can easily add to wrapperFs down the line
type wrapperFs struct {
@ -18,24 +18,24 @@ type wrapperFs struct {
fs.FS
}
// FilesystemMap stores a map of filesystems
// FileSystemMap stores a map of filesystems
// the empty key will be overwritten to be the default key
// it includes a default filesystem, based off the os fs
type FilesystemMap struct {
type FileSystemMap struct {
m sync.Map
}
// note that the first invocation of key cannot be called in a racy context.
func (f *FilesystemMap) key(k string) string {
func (f *FileSystemMap) key(k string) string {
if k == "" {
k = DefaultFilesystemKey
k = DefaultFileSystemKey
}
return k
}
// Register will add the filesystem with key to later be retrieved
// A call with a nil fs will call unregister, ensuring that a call to Default() will never be nil
func (f *FilesystemMap) Register(k string, v fs.FS) {
func (f *FileSystemMap) Register(k string, v fs.FS) {
k = f.key(k)
if v == nil {
f.Unregister(k)
@ -47,23 +47,23 @@ func (f *FilesystemMap) Register(k string, v fs.FS) {
// Unregister will remove the filesystem with key from the filesystem map
// if the key is the default key, it will set the default to the osFS instead of deleting it
// modules should call this on cleanup to be safe
func (f *FilesystemMap) Unregister(k string) {
func (f *FileSystemMap) Unregister(k string) {
k = f.key(k)
if k == DefaultFilesystemKey {
f.m.Store(k, DefaultFilesystem)
if k == DefaultFileSystemKey {
f.m.Store(k, DefaultFileSystem)
} else {
f.m.Delete(k)
}
}
// Get will get a filesystem with a given key
func (f *FilesystemMap) Get(k string) (v fs.FS, ok bool) {
func (f *FileSystemMap) Get(k string) (v fs.FS, ok bool) {
k = f.key(k)
c, ok := f.m.Load(strings.TrimSpace(k))
if !ok {
if k == DefaultFilesystemKey {
f.m.Store(k, DefaultFilesystem)
return DefaultFilesystem, true
if k == DefaultFileSystemKey {
f.m.Store(k, DefaultFileSystem)
return DefaultFileSystem, true
}
return nil, ok
}
@ -71,7 +71,7 @@ func (f *FilesystemMap) Get(k string) (v fs.FS, ok bool) {
}
// Default will get the default filesystem in the filesystem map
func (f *FilesystemMap) Default() fs.FS {
val, _ := f.Get(DefaultFilesystemKey)
func (f *FileSystemMap) Default() fs.FS {
val, _ := f.Get(DefaultFileSystemKey)
return val
}

View File

@ -20,9 +20,7 @@ import (
"errors"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
@ -206,27 +204,26 @@ func (app *App) On(eventName string, handler Handler) error {
//
// Note that the data map is not copied, for efficiency. After Emit() is called, the
// data passed in should not be changed in other goroutines.
func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) Event {
func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) caddy.Event {
logger := app.logger.With(zap.String("name", eventName))
id, err := uuid.NewRandom()
e, err := caddy.NewEvent(ctx, eventName, data)
if err != nil {
logger.Error("failed generating new event ID", zap.Error(err))
logger.Error("failed to create event", zap.Error(err))
}
eventName = strings.ToLower(eventName)
e := Event{
Data: data,
id: id,
ts: time.Now(),
name: eventName,
origin: ctx.Module(),
var originModule caddy.ModuleInfo
var originModuleID caddy.ModuleID
var originModuleName string
if origin := e.Origin(); origin != nil {
originModule = origin.CaddyModule()
originModuleID = originModule.ID
originModuleName = originModule.String()
}
logger = logger.With(
zap.String("id", e.id.String()),
zap.String("origin", e.origin.CaddyModule().String()))
zap.String("id", e.ID().String()),
zap.String("origin", originModuleName))
// add event info to replacer, make sure it's in the context
repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
@ -239,15 +236,15 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E
case "event":
return e, true
case "event.id":
return e.id, true
return e.ID(), true
case "event.name":
return e.name, true
return e.Name(), true
case "event.time":
return e.ts, true
return e.Timestamp(), true
case "event.time_unix":
return e.ts.UnixMilli(), true
return e.Timestamp().UnixMilli(), true
case "event.module":
return e.origin.CaddyModule().ID, true
return originModuleID, true
case "event.data":
return e.Data, true
}
@ -269,7 +266,7 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E
// invoke handlers bound to the event by name and also all events; this for loop
// iterates twice at most: once for the event name, once for "" (all events)
for {
moduleID := e.origin.CaddyModule().ID
moduleID := originModuleID
// implement propagation up the module tree (i.e. start with "a.b.c" then "a.b" then "a" then "")
for app.subscriptions[eventName] != nil {
@ -288,7 +285,7 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E
zap.Any("handler", handler))
if err := handler.Handle(ctx, e); err != nil {
aborted := errors.Is(err, ErrAborted)
aborted := errors.Is(err, caddy.ErrEventAborted)
logger.Error("handler error",
zap.Error(err),
@ -322,76 +319,9 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E
return e
}
// Event represents something that has happened or is happening.
// An Event value is not synchronized, so it should be copied if
// being used in goroutines.
//
// EXPERIMENTAL: As with the rest of this package, events are
// subject to change.
type Event struct {
// If non-nil, the event has been aborted, meaning
// propagation has stopped to other handlers and
// the code should stop what it was doing. Emitters
// may choose to use this as a signal to adjust their
// code path appropriately.
Aborted error
// The data associated with the event. Usually the
// original emitter will be the only one to set or
// change these values, but the field is exported
// so handlers can have full access if needed.
// However, this map is not synchronized, so
// handlers must not use this map directly in new
// goroutines; instead, copy the map to use it in a
// goroutine.
Data map[string]any
id uuid.UUID
ts time.Time
name string
origin caddy.Module
}
func (e Event) ID() uuid.UUID { return e.id }
func (e Event) Timestamp() time.Time { return e.ts }
func (e Event) Name() string { return e.name }
func (e Event) Origin() caddy.Module { return e.origin }
// CloudEvent exports event e as a structure that, when
// serialized as JSON, is compatible with the
// CloudEvents spec.
func (e Event) CloudEvent() CloudEvent {
dataJSON, _ := json.Marshal(e.Data)
return CloudEvent{
ID: e.id.String(),
Source: e.origin.CaddyModule().String(),
SpecVersion: "1.0",
Type: e.name,
Time: e.ts,
DataContentType: "application/json",
Data: dataJSON,
}
}
// CloudEvent is a JSON-serializable structure that
// is compatible with the CloudEvents specification.
// See https://cloudevents.io.
type CloudEvent struct {
ID string `json:"id"`
Source string `json:"source"`
SpecVersion string `json:"specversion"`
Type string `json:"type"`
Time time.Time `json:"time"`
DataContentType string `json:"datacontenttype,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
}
// ErrAborted cancels an event.
var ErrAborted = errors.New("event aborted")
// Handler is a type that can handle events.
type Handler interface {
Handle(context.Context, Event) error
Handle(context.Context, caddy.Event) error
}
// Interface guards

View File

@ -69,11 +69,11 @@ func (xs *Filesystems) Provision(ctx caddy.Context) error {
}
// register that module
ctx.Logger().Debug("registering fs", zap.String("fs", f.Key))
ctx.Filesystems().Register(f.Key, f.fileSystem)
ctx.FileSystems().Register(f.Key, f.fileSystem)
// remember to unregister the module when we are done
xs.defers = append(xs.defers, func() {
ctx.Logger().Debug("unregistering fs", zap.String("fs", f.Key))
ctx.Filesystems().Unregister(f.Key)
ctx.FileSystems().Unregister(f.Key)
})
}
return nil

View File

@ -274,7 +274,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
func (m *MatchFile) Provision(ctx caddy.Context) error {
m.logger = ctx.Logger()
m.fsmap = ctx.Filesystems()
m.fsmap = ctx.FileSystems()
if m.Root == "" {
m.Root = "{http.vars.root}"

View File

@ -117,7 +117,7 @@ func TestFileMatcher(t *testing.T) {
},
} {
m := &MatchFile{
fsmap: &filesystems.FilesystemMap{},
fsmap: &filesystems.FileSystemMap{},
Root: "./testdata",
TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"},
}
@ -229,7 +229,7 @@ func TestPHPFileMatcher(t *testing.T) {
},
} {
m := &MatchFile{
fsmap: &filesystems.FilesystemMap{},
fsmap: &filesystems.FileSystemMap{},
Root: "./testdata",
TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php"},
SplitPath: []string{".php"},
@ -273,7 +273,7 @@ func TestPHPFileMatcher(t *testing.T) {
func TestFirstSplit(t *testing.T) {
m := MatchFile{
SplitPath: []string{".php"},
fsmap: &filesystems.FilesystemMap{},
fsmap: &filesystems.FileSystemMap{},
}
actual, remainder := m.firstSplit("index.PHP/somewhere")
expected := "index.PHP"

View File

@ -186,7 +186,7 @@ func (FileServer) CaddyModule() caddy.ModuleInfo {
func (fsrv *FileServer) Provision(ctx caddy.Context) error {
fsrv.logger = ctx.Logger()
fsrv.fsmap = ctx.Filesystems()
fsrv.fsmap = ctx.FileSystems()
if fsrv.FileSystem == "" {
fsrv.FileSystem = "{http.vars.fs}"

View File

@ -660,7 +660,8 @@ nextName:
var httpsRec libdns.Record
var nameHasExistingRecord bool
for _, rec := range recs {
if rec.Name == relName {
// TODO: providers SHOULD normalize root-level records to be named "@"; remove the extra conditions when the transition to the new semantics is done
if rec.Name == relName || (rec.Name == "" && relName == "@") {
// CNAME records are exclusive of all other records, so we cannot publish an HTTPS
// record for a domain that is CNAME'd. See #6922.
if rec.Type == "CNAME" {