core: Save app provisioning errors with context (#7070)

* fix(provisioning): remove app from apps map when its provision failed

* Clean up failed app provisioning with defer

* fix(provisioning): record apps that failed to provision with their error

* save the error when an app fails to initialize and return this error when this app is requested by a module

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Co-authored-by: WeidiDeng <weidi_deng@icloud.com>
This commit is contained in:
Alexandre Daubois 2025-07-29 18:31:13 +02:00 committed by GitHub
parent b7ae39e906
commit fe41ff3c5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 22 additions and 3 deletions

View File

@ -82,6 +82,9 @@ type Config struct {
AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="` AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="`
apps map[string]App apps map[string]App
// failedApps is a map of apps that failed to provision with their underlying error.
failedApps map[string]error
storage certmagic.Storage storage certmagic.Storage
eventEmitter eventEmitter eventEmitter eventEmitter
@ -522,6 +525,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
// prepare the new config for use // prepare the new config for use
newCfg.apps = make(map[string]App) newCfg.apps = make(map[string]App)
newCfg.failedApps = make(map[string]error)
// set up global storage and make it CertMagic's default storage, too // set up global storage and make it CertMagic's default storage, too
err = func() error { err = func() error {

View File

@ -393,6 +393,8 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
return nil, fmt.Errorf("module value cannot be null") return nil, fmt.Errorf("module value cannot be null")
} }
var err error
// if this is an app module, keep a reference to it, // if this is an app module, keep a reference to it,
// since submodules may need to reference it during // since submodules may need to reference it during
// provisioning (even though the parent app module // provisioning (even though the parent app module
@ -402,12 +404,17 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
// module has been configured for DNS challenges) // module has been configured for DNS challenges)
if appModule, ok := val.(App); ok { if appModule, ok := val.(App); ok {
ctx.cfg.apps[id] = appModule ctx.cfg.apps[id] = appModule
defer func() {
if err != nil {
ctx.cfg.failedApps[id] = err
}
}()
} }
ctx.ancestry = append(ctx.ancestry, val) ctx.ancestry = append(ctx.ancestry, val)
if prov, ok := val.(Provisioner); ok { if prov, ok := val.(Provisioner); ok {
err := prov.Provision(ctx) err = prov.Provision(ctx)
if err != nil { if err != nil {
// incomplete provisioning could have left state // incomplete provisioning could have left state
// dangling, so make sure it gets cleaned up // dangling, so make sure it gets cleaned up
@ -422,7 +429,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
} }
if validator, ok := val.(Validator); ok { if validator, ok := val.(Validator); ok {
err := validator.Validate() err = validator.Validate()
if err != nil { if err != nil {
// since the module was already provisioned, make sure we clean up // since the module was already provisioned, make sure we clean up
if cleanerUpper, ok := val.(CleanerUpper); ok { if cleanerUpper, ok := val.(CleanerUpper); ok {
@ -487,6 +494,10 @@ func (ctx Context) loadModuleInline(moduleNameKey, moduleScope string, raw json.
// or stop App modules. The caller is expected to assert to the // or stop App modules. The caller is expected to assert to the
// concrete type. // concrete type.
func (ctx Context) App(name string) (any, error) { func (ctx Context) App(name string) (any, error) {
// if the app failed to load before, return the cached error
if err, ok := ctx.cfg.failedApps[name]; ok {
return nil, fmt.Errorf("loading %s app module: %v", name, err)
}
if app, ok := ctx.cfg.apps[name]; ok { if app, ok := ctx.cfg.apps[name]; ok {
return app, nil return app, nil
} }
@ -511,6 +522,10 @@ func (ctx Context) AppIfConfigured(name string) (any, error) {
if ctx.cfg == nil { if ctx.cfg == nil {
return nil, fmt.Errorf("app module %s: %w", name, ErrNotConfigured) return nil, fmt.Errorf("app module %s: %w", name, ErrNotConfigured)
} }
// if the app failed to load before, return the cached error
if err, ok := ctx.cfg.failedApps[name]; ok {
return nil, fmt.Errorf("loading %s app module: %v", name, err)
}
if app, ok := ctx.cfg.apps[name]; ok { if app, ok := ctx.cfg.apps[name]; ok {
return app, nil return app, nil
} }