From b9809a32183271b71356602266ee47d9c92b51d6 Mon Sep 17 00:00:00 2001 From: Andreas Kohn Date: Thu, 6 Jun 2024 16:28:25 +0200 Subject: [PATCH 1/4] Split `run` into a public `BuildContext` and a private part `BuildContext` can be used to set up a caddy context from a config, but not start any listeners or active components: The returned context has the configured apps provisioned, but otherwise is inert. This is EXPERIMENTAL: Minimally it's missing documentation and the example for how this can be used to run unit tests. --- caddy.go | 93 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 49 insertions(+), 44 deletions(-) diff --git a/caddy.go b/caddy.go index 27acabb1267..f28a48856c9 100644 --- a/caddy.go +++ b/caddy.go @@ -397,6 +397,53 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error { // will want to use Run instead, which also // updates the config's raw state. func run(newCfg *Config, start bool) (Context, error) { + ctx, err := BuildContext(newCfg, start) + if err != nil { + return ctx, err + } + + if !start { + return ctx, nil + } + + // Provision any admin routers which may need to access + // some of the other apps at runtime + err = newCfg.Admin.provisionAdminRouters(ctx) + if err != nil { + return ctx, err + } + + // Start + err = func() error { + started := make([]string, 0, len(newCfg.apps)) + for name, a := range newCfg.apps { + err := a.Start() + if err != nil { + // an app failed to start, so we need to stop + // all other apps that were already started + for _, otherAppName := range started { + err2 := newCfg.apps[otherAppName].Stop() + if err2 != nil { + err = fmt.Errorf("%v; additionally, aborting app %s: %v", + err, otherAppName, err2) + } + } + return fmt.Errorf("%s app module: start: %v", name, err) + } + started = append(started, name) + } + return nil + }() + if err != nil { + return ctx, err + } + + // 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, newCfg) +} + +func BuildContext(newCfg *Config, replaceAdminServer bool) (Context, error) { // because we will need to roll back any state // modifications if this function errors, we // keep a single error value and scope all @@ -444,7 +491,7 @@ func run(newCfg *Config, start bool) (Context, error) { } // start the admin endpoint (and stop any prior one) - if start { + if replaceAdminServer { err = replaceLocalAdminServer(newCfg) if err != nil { return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err) @@ -491,49 +538,7 @@ func run(newCfg *Config, start bool) (Context, error) { } return nil }() - if err != nil { - return ctx, err - } - - if !start { - return ctx, nil - } - - // Provision any admin routers which may need to access - // some of the other apps at runtime - err = newCfg.Admin.provisionAdminRouters(ctx) - if err != nil { - return ctx, err - } - - // Start - err = func() error { - started := make([]string, 0, len(newCfg.apps)) - for name, a := range newCfg.apps { - err := a.Start() - if err != nil { - // an app failed to start, so we need to stop - // all other apps that were already started - for _, otherAppName := range started { - err2 := newCfg.apps[otherAppName].Stop() - if err2 != nil { - err = fmt.Errorf("%v; additionally, aborting app %s: %v", - err, otherAppName, err2) - } - } - return fmt.Errorf("%s app module: start: %v", name, err) - } - started = append(started, name) - } - return nil - }() - if err != nil { - return ctx, err - } - - // 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, newCfg) + return ctx, err } // finishSettingUp should be run after all apps have successfully started. From 86401566220454d995dad225791c7d11f8ed5f3d Mon Sep 17 00:00:00 2001 From: Andreas Kohn Date: Thu, 6 Jun 2024 18:34:37 +0200 Subject: [PATCH 2/4] Use the config from the context The config passed into `BuildContext` can be nil, in which case `BuildContext` will just make one up that works. In either case that will end up in the finished context. --- caddy.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/caddy.go b/caddy.go index f28a48856c9..c11fb8b674c 100644 --- a/caddy.go +++ b/caddy.go @@ -408,21 +408,21 @@ func run(newCfg *Config, start bool) (Context, error) { // Provision any admin routers which may need to access // some of the other apps at runtime - err = newCfg.Admin.provisionAdminRouters(ctx) + err = ctx.cfg.Admin.provisionAdminRouters(ctx) if err != nil { return ctx, err } // Start err = func() error { - started := make([]string, 0, len(newCfg.apps)) - for name, a := range newCfg.apps { + started := make([]string, 0, len(ctx.cfg.apps)) + for name, a := range ctx.cfg.apps { err := a.Start() if err != nil { // an app failed to start, so we need to stop // all other apps that were already started for _, otherAppName := range started { - err2 := newCfg.apps[otherAppName].Stop() + err2 := ctx.cfg.apps[otherAppName].Stop() if err2 != nil { err = fmt.Errorf("%v; additionally, aborting app %s: %v", err, otherAppName, err2) @@ -440,7 +440,7 @@ func run(newCfg *Config, start bool) (Context, error) { // 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, newCfg) + return ctx, finishSettingUp(ctx, ctx.cfg) } func BuildContext(newCfg *Config, replaceAdminServer bool) (Context, error) { From 69286cd201cf711bfd325fb288987e9a23016857 Mon Sep 17 00:00:00 2001 From: Andreas Kohn Date: Thu, 6 Jun 2024 22:15:19 +0200 Subject: [PATCH 3/4] Rename `BuildContext` to `ProvisionContext` to better match the function --- caddy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/caddy.go b/caddy.go index c11fb8b674c..39d44c9c30b 100644 --- a/caddy.go +++ b/caddy.go @@ -397,7 +397,7 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error { // will want to use Run instead, which also // updates the config's raw state. func run(newCfg *Config, start bool) (Context, error) { - ctx, err := BuildContext(newCfg, start) + ctx, err := ProvisionContext(newCfg, start) if err != nil { return ctx, err } @@ -443,7 +443,7 @@ func run(newCfg *Config, start bool) (Context, error) { return ctx, finishSettingUp(ctx, ctx.cfg) } -func BuildContext(newCfg *Config, replaceAdminServer bool) (Context, error) { +func ProvisionContext(newCfg *Config, replaceAdminServer bool) (Context, error) { // because we will need to roll back any state // modifications if this function errors, we // keep a single error value and scope all From 264ff4dededa9829c88833f734ed4b7b4325d200 Mon Sep 17 00:00:00 2001 From: Andreas Kohn Date: Thu, 6 Jun 2024 22:21:53 +0200 Subject: [PATCH 4/4] Hide the `replaceAdminServer` parts The admin server is a global thing, and in the envisioned use case for `ProvisionContext` shouldn't actually exist. Hide this detail in a private `provisionContext` instead, and only expose it publicly with `replaceAdminServer` set to `false`. This should reduce foot-shooting potential further; in addition the documentation comment now clearly spells out that the exact interface and implementation details of `ProvisionContext` are experimental and subject to change. --- caddy.go | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/caddy.go b/caddy.go index 39d44c9c30b..7dd989c9e1e 100644 --- a/caddy.go +++ b/caddy.go @@ -397,7 +397,7 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error { // will want to use Run instead, which also // updates the config's raw state. func run(newCfg *Config, start bool) (Context, error) { - ctx, err := ProvisionContext(newCfg, start) + ctx, err := provisionContext(newCfg, start) if err != nil { return ctx, err } @@ -443,7 +443,12 @@ func run(newCfg *Config, start bool) (Context, error) { return ctx, finishSettingUp(ctx, ctx.cfg) } -func ProvisionContext(newCfg *Config, replaceAdminServer bool) (Context, error) { +// provisionContext creates a new context from the given configuration and provisions +// storage and apps. +// If `newCfg` is nil a new empty configuration will be created. +// If `replaceAdminServer` is true any currently active admin server will be replaced +// with a new admin server based on the provided configuration. +func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error) { // because we will need to roll back any state // modifications if this function errors, we // keep a single error value and scope all @@ -541,6 +546,15 @@ func ProvisionContext(newCfg *Config, replaceAdminServer bool) (Context, error) return ctx, err } +// ProvisionContext creates a new context from the configuration and provisions storage +// and app modules. +// The function is intended for testing and advanced use cases only, typically `Run` should be +// use to ensure a fully functional caddy instance. +// EXPERIMENTAL: While this is public the interface and implementation details of this function may change. +func ProvisionContext(newCfg *Config) (Context, error) { + return provisionContext(newCfg, false) +} + // finishSettingUp should be run after all apps have successfully started. func finishSettingUp(ctx Context, cfg *Config) error { // establish this server's identity (only after apps are loaded