From e0148cbe17a51f039293deec778d958d738d3c49 Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:00:08 +0200 Subject: [PATCH 01/29] fix(core): return error instead of panicking in WithOwner mutator The ObjectMutator signature already propagates errors; panicking on SetOwnerReference failure would crash the whole controller-manager instead of failing the single reconciliation. --- internal/core/utils.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/internal/core/utils.go b/internal/core/utils.go index 8b16454f..e9dd8873 100644 --- a/internal/core/utils.go +++ b/internal/core/utils.go @@ -84,11 +84,7 @@ func WithController[T client.Object](scheme *k8sruntime.Scheme, owner client.Obj func WithOwner[T client.Object](scheme *k8sruntime.Scheme, owner client.Object) ObjectMutator[T] { return func(t T) error { - if err := controllerutil.SetOwnerReference(owner, t, scheme); err != nil { - panic(err) - } - - return nil + return controllerutil.SetOwnerReference(owner, t, scheme) } } From b8936d5059435769c09ea6cb5983fdd74245a57a Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:00:30 +0200 Subject: [PATCH 02/29] fix(core): propagate conversion error in GetAllStackDependencies GetAllStackDependencies already returns an error; panicking on a FromUnstructured conversion failure would crash the controller-manager on a single malformed resource instead of failing one reconciliation. --- internal/core/stacks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/core/stacks.go b/internal/core/stacks.go index f9e848bb..28f93f61 100644 --- a/internal/core/stacks.go +++ b/internal/core/stacks.go @@ -37,7 +37,7 @@ func GetAllStackDependencies(ctx Context, stackName string, to any) error { for _, item := range list.Items { t := reflect.New(objectType.Elem()).Interface().(client.Object) if err := runtime.DefaultUnstructuredConverter.FromUnstructured(item.Object, t); err != nil { - panic(err) + return errors.Wrapf(err, "converting unstructured object '%s' to %s", item.GetName(), objectType) } ret = reflect.Append(ret, reflect.ValueOf(t)) } From d08f5de15ffb2f4b5d9d66e7ed432f95eb4878ad Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:00:45 +0200 Subject: [PATCH 03/29] fix(settings): propagate marshal error in GetAs GetAs already returns an error; panicking on json.Marshal failure would crash the controller-manager instead of failing the single settings lookup. --- internal/resources/settings/helpers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/resources/settings/helpers.go b/internal/resources/settings/helpers.go index ddb3593e..e15dc943 100644 --- a/internal/resources/settings/helpers.go +++ b/internal/resources/settings/helpers.go @@ -384,7 +384,7 @@ func GetAs[T any](ctx core.Context, stack string, keys ...string) (*T, error) { data, err := json.Marshal(m) if err != nil { - panic(err) + return nil, err } if err := json.Unmarshal(data, &ret); err != nil { From 28ee590841ab00f7c0a8c8f0ac8817912af804e0 Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:01:07 +0200 Subject: [PATCH 04/29] fix(resourcereferences): propagate SetNestedMap error in Reconcile The CreateOrUpdate mutate closure already returns an error; panicking would crash the controller-manager instead of failing the single ResourceReference reconciliation. --- internal/resources/resourcereferences/init.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/resources/resourcereferences/init.go b/internal/resources/resourcereferences/init.go index e90e0022..b2ea1865 100644 --- a/internal/resources/resourcereferences/init.go +++ b/internal/resources/resourcereferences/init.go @@ -158,7 +158,7 @@ func Reconcile(ctx core.Context, stack *v1beta1.Stack, req *v1beta1.ResourceRefe } if err := unstructured.SetNestedMap(content, annotations, "metadata", "annotations"); err != nil { - panic(err) + return errors.Wrap(err, "setting annotations on replicated resource") } hasOwnerReference, err := core.HasOwnerReference(ctx, req, newResource) From e1835ac57acf1acedaa44a6ac39117e0a65e50b3 Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:01:26 +0200 Subject: [PATCH 05/29] fix(benthos): propagate config map hashing error The enclosing reconcile helper already returns an error; panicking on a hashing failure would crash the controller-manager instead of failing the single Benthos reconciliation. --- internal/resources/benthos/controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/resources/benthos/controller.go b/internal/resources/benthos/controller.go index 77a740c7..b5bd8a1c 100644 --- a/internal/resources/benthos/controller.go +++ b/internal/resources/benthos/controller.go @@ -319,7 +319,7 @@ func createDeployment(ctx Context, stack *v1beta1.Stack, b *v1beta1.Benthos) err digest := sha256.New() for _, configMap := range configMaps { if err := json.NewEncoder(digest).Encode(configMap.Data); err != nil { - panic(err) + return errors.Wrap(err, "hashing config map data") } } for _, stream := range streams { From 481d817cbe5610ac8ab20b0aca4c7c9d79bb9c6b Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:01:43 +0200 Subject: [PATCH 06/29] fix(api): return parse error from URI.UnmarshalJSON instead of panicking UnmarshalJSON runs while decoding user-provided custom resources: a single CR carrying an unparseable URL would crash the whole controller-manager. Returning the error fails only the decode of the offending object. --- api/formance.com/v1beta1/shared.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/formance.com/v1beta1/shared.go b/api/formance.com/v1beta1/shared.go index 5894e65c..64537d22 100644 --- a/api/formance.com/v1beta1/shared.go +++ b/api/formance.com/v1beta1/shared.go @@ -343,7 +343,7 @@ func (u *URI) UnmarshalJSON(data []byte) error { v, err := url.Parse(s) if err != nil { - panic(err) + return err } *u = URI{ From bd995b9391fcd0c47086436c9ef109b7aaeff4a2 Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:02:10 +0200 Subject: [PATCH 07/29] fix(api): use utilruntime.Must for init-time equality registration Registering the URI semantic-equality function can only fail on a programmer error and must abort startup; utilruntime.Must is the established idiom for this (already used in cmd/main.go) and reports the failure through the k8s error handlers instead of a bare panic. --- api/formance.com/v1beta1/shared.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/api/formance.com/v1beta1/shared.go b/api/formance.com/v1beta1/shared.go index 64537d22..003b0796 100644 --- a/api/formance.com/v1beta1/shared.go +++ b/api/formance.com/v1beta1/shared.go @@ -9,6 +9,7 @@ import ( "golang.org/x/mod/semver" "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/formancehq/go-libs/v5/pkg/types/pointer" @@ -372,7 +373,7 @@ func ParseURL(v string) (*URI, error) { } func init() { - if err := equality.Semantic.AddFunc(func(a, b *URI) bool { + utilruntime.Must(equality.Semantic.AddFunc(func(a, b *URI) bool { if a == nil && b != nil { return false } @@ -383,9 +384,7 @@ func init() { return true } return a.String() == b.String() - }); err != nil { - panic(err) - } + })) } const ( From 6f5f0aa570339d1fe228dc524aa2c46b1043f767 Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:02:26 +0200 Subject: [PATCH 08/29] fix(client): use utilruntime.Must for scheme registration in init Same idiom as cmd/main.go: scheme registration failure is a programmer error that must abort startup; utilruntime.Must reports it through the k8s error handlers instead of a bare panic. --- pkg/client/formance.com/v1beta1/client.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/client/formance.com/v1beta1/client.go b/pkg/client/formance.com/v1beta1/client.go index b0b43011..8eb8f83f 100644 --- a/pkg/client/formance.com/v1beta1/client.go +++ b/pkg/client/formance.com/v1beta1/client.go @@ -1,6 +1,7 @@ package v1beta1 import ( + utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" @@ -8,9 +9,7 @@ import ( ) func init() { - if err := v1beta1.AddToScheme(scheme.Scheme); err != nil { - panic(err) - } + utilruntime.Must(v1beta1.AddToScheme(scheme.Scheme)) } type Client struct { From 638445b5dcff0fbf73d083054d7d19da3590d16a Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:02:56 +0200 Subject: [PATCH 09/29] fix(core): remove dead CopyDir helper CopyDir has no callers outside its own recursion and contained two panic() calls on filesystem errors. Removing it eliminates both panic sites. --- internal/core/utils.go | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/internal/core/utils.go b/internal/core/utils.go index e9dd8873..456d7d5f 100644 --- a/internal/core/utils.go +++ b/internal/core/utils.go @@ -6,8 +6,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "io/fs" - "path/filepath" "reflect" "runtime" "strings" @@ -48,27 +46,6 @@ func HashFromResources(resources ...*unstructured.Unstructured) string { return base64.StdEncoding.EncodeToString(digest.Sum(nil)) } -func CopyDir(f fs.FS, root, path string, ret *map[string]string) { - dirEntries, err := fs.ReadDir(f, path) - if err != nil { - panic(err) - } - for _, dirEntry := range dirEntries { - dirEntryPath := filepath.Join(path, dirEntry.Name()) - if dirEntry.IsDir() { - CopyDir(f, root, dirEntryPath, ret) - } else { - fileContent, err := fs.ReadFile(f, dirEntryPath) - if err != nil { - panic(err) - } - sanitizedPath := strings.TrimPrefix(dirEntryPath, root) - sanitizedPath = strings.TrimPrefix(sanitizedPath, "/") - (*ret)[sanitizedPath] = string(fileContent) - } - } -} - type ObjectMutator[T any] func(t T) error func WithController[T client.Object](scheme *k8sruntime.Scheme, owner client.Object) ObjectMutator[T] { From fe4f195a026320e7958f946e524b9294928dfd81 Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:03:38 +0200 Subject: [PATCH 10/29] fix(core): log and drop event instead of panicking when listing stacks in WithWatchVersions A transient API-server error while listing stacks in the Versions watch handler would crash the whole controller-manager. Log the error and drop the event instead: the next Versions event re-triggers the mapping. --- internal/core/reconciler.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/core/reconciler.go b/internal/core/reconciler.go index 00824b70..f8d95d15 100644 --- a/internal/core/reconciler.go +++ b/internal/core/reconciler.go @@ -403,11 +403,13 @@ func WithModuleReconciler[T v1beta1.Module](fn func(ctx Context, stack *v1beta1. func WithWatchVersions[T client.Object](options *ReconcilerOptions[T]) { reconcileModule := func(ctx context.Context, mgr Manager, target client.Object, versionFileName string, limitingInterface workqueue.TypedRateLimitingInterface[reconcile.Request]) { + logger := log.FromContext(ctx) stackList := &v1beta1.StackList{} if err := mgr.GetClient().List(ctx, stackList, client.MatchingFields{ ".spec.versionsFromFile": versionFileName, }); err != nil { - panic(err) + logger.Error(err, "listing stacks for versions file, dropping event", "versionsFile", versionFileName) + return } kinds, _, err := mgr.GetScheme().ObjectKinds(target) From d114050c8c21c782a45edf6c11d384f6bd7bce2b Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:03:54 +0200 Subject: [PATCH 11/29] fix(core): log and drop event instead of panicking on kind resolution in WithWatchVersions ObjectKinds failing for the watch target is a registration problem; crashing the controller-manager from an event handler hides the root cause. Log the error and drop the event instead. --- internal/core/reconciler.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/core/reconciler.go b/internal/core/reconciler.go index f8d95d15..7a658028 100644 --- a/internal/core/reconciler.go +++ b/internal/core/reconciler.go @@ -414,7 +414,8 @@ func WithWatchVersions[T client.Object](options *ReconcilerOptions[T]) { kinds, _, err := mgr.GetScheme().ObjectKinds(target) if err != nil { - panic(err) + logger.Error(err, "resolving object kind, dropping event", "target", fmt.Sprintf("%T", target)) + return } for _, stack := range stackList.Items { From aa9b68db65b2cc360e226350bf600f84c26fc910 Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:04:09 +0200 Subject: [PATCH 12/29] fix(core): skip stack instead of panicking when listing modules in WithWatchVersions A transient list error for one stack would crash the whole controller-manager and lose the events of every other stack. Log the error and continue with the remaining stacks. --- internal/core/reconciler.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/core/reconciler.go b/internal/core/reconciler.go index 7a658028..72db9250 100644 --- a/internal/core/reconciler.go +++ b/internal/core/reconciler.go @@ -424,7 +424,8 @@ func WithWatchVersions[T client.Object](options *ReconcilerOptions[T]) { if err := mgr.GetClient().List(ctx, list, client.MatchingFields{ "stack": stack.Name, }); err != nil { - panic(err) + logger.Error(err, "listing modules for stack, skipping it", "stack", stack.Name) + continue } for _, item := range list.Items { From cf331c6d85cdf8b83f7d05acc69de9b6d82b9b54 Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:04:29 +0200 Subject: [PATCH 13/29] fix(core): log and drop event instead of panicking in Versions UpdateFunc Same rationale as the other WithWatchVersions handlers: an event handler must not crash the controller-manager on a kind-resolution error. --- internal/core/reconciler.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/core/reconciler.go b/internal/core/reconciler.go index 72db9250..2bc3ca74 100644 --- a/internal/core/reconciler.go +++ b/internal/core/reconciler.go @@ -450,7 +450,8 @@ func WithWatchVersions[T client.Object](options *ReconcilerOptions[T]) { kinds, _, err := mgr.GetScheme().ObjectKinds(target) if err != nil { - panic(err) + log.FromContext(ctx).Error(err, "resolving object kind, dropping event", "target", fmt.Sprintf("%T", target)) + return } kind := strings.ToLower(kinds[0].Kind) if oldObject.Spec[kind] == newObject.Spec[kind] { From df6b49ceb974feed1b47ce75fe81dcde75b7f847 Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:07:09 +0200 Subject: [PATCH 14/29] fix(core): make LowerCamelCaseKind return an error instead of panicking A kind-resolution failure (unregistered type) would crash the whole controller-manager from any module deployment helper. The function now returns the error; the eleven callers propagate it through their existing error paths, and the databases watch handler logs it and drops the event. --- internal/core/module.go | 6 +++--- internal/resources/auths/deployment.go | 6 +++++- internal/resources/caddy/caddy.go | 6 +++++- internal/resources/databases/watch.go | 10 ++++++++-- internal/resources/ledgers/deployments.go | 6 +++++- internal/resources/orchestrations/deployments.go | 6 +++++- internal/resources/payments/deployments.go | 6 +++++- internal/resources/reconciliations/deployments.go | 6 +++++- internal/resources/stargates/deployment.go | 6 +++++- internal/resources/transactionplane/deployments.go | 6 +++++- internal/resources/wallets/deployment.go | 6 +++++- internal/resources/webhooks/deployment.go | 6 +++++- 12 files changed, 61 insertions(+), 15 deletions(-) diff --git a/internal/core/module.go b/internal/core/module.go index aca96906..d26d1d5d 100644 --- a/internal/core/module.go +++ b/internal/core/module.go @@ -7,12 +7,12 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -func LowerCamelCaseKind(ctx Context, ob client.Object) string { +func LowerCamelCaseKind(ctx Context, ob client.Object) (string, error) { kinds, _, err := ctx.GetScheme().ObjectKinds(ob) if err != nil { - panic(err) + return "", err } - return strcase.ToLowerCamel(kinds[0].Kind) + return strcase.ToLowerCamel(kinds[0].Kind), nil } func LowerCaseKind(ctx Context, ob client.Object) string { diff --git a/internal/resources/auths/deployment.go b/internal/resources/auths/deployment.go index 5b9f84e7..be03e314 100644 --- a/internal/resources/auths/deployment.go +++ b/internal/resources/auths/deployment.go @@ -39,7 +39,11 @@ func createDeployment(ctx Context, stack *v1beta1.Stack, auth *v1beta1.Auth, dat } env := make([]corev1.EnvVar, 0) - otlpEnv, err := settings.GetOTELEnvVars(ctx, stack.Name, LowerCamelCaseKind(ctx, auth), " ") + serviceName, err := LowerCamelCaseKind(ctx, auth) + if err != nil { + return err + } + otlpEnv, err := settings.GetOTELEnvVars(ctx, stack.Name, serviceName, " ") if err != nil { return err } diff --git a/internal/resources/caddy/caddy.go b/internal/resources/caddy/caddy.go index 3c2fc9ca..c61c9b7f 100644 --- a/internal/resources/caddy/caddy.go +++ b/internal/resources/caddy/caddy.go @@ -29,7 +29,11 @@ func DeploymentTemplate( ) (*appsv1.Deployment, error) { t := &appsv1.Deployment{} - otlpEnv, err := settings.GetOTELEnvVars(ctx, stack.Name, core.LowerCamelCaseKind(ctx, owner), ",") + serviceName, err := core.LowerCamelCaseKind(ctx, owner) + if err != nil { + return nil, err + } + otlpEnv, err := settings.GetOTELEnvVars(ctx, stack.Name, serviceName, ",") if err != nil { return nil, err } diff --git a/internal/resources/databases/watch.go b/internal/resources/databases/watch.go index ce1347ec..70aa5c11 100644 --- a/internal/resources/databases/watch.go +++ b/internal/resources/databases/watch.go @@ -4,6 +4,7 @@ import ( "reflect" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/formancehq/operator/v3/api/formance.com/v1beta1" @@ -14,13 +15,18 @@ func Watch[T client.Object]() core.ReconcilerOption[T] { var t T t = reflect.New(reflect.TypeOf(t).Elem()).Interface().(T) return core.WithWatch[T, *v1beta1.Database](func(ctx core.Context, database *v1beta1.Database) []reconcile.Request { - if database.Spec.Service != core.LowerCamelCaseKind(ctx, t) { + serviceName, err := core.LowerCamelCaseKind(ctx, t) + if err != nil { + log.FromContext(ctx).Error(err, "resolving object kind, dropping event") + return []reconcile.Request{} + } + if database.Spec.Service != serviceName { return []reconcile.Request{} } slice := reflect.MakeSlice(reflect.SliceOf(reflect.TypeOf(t)), 0, 0).Interface() - err := core.GetAllStackDependencies(ctx, database.Spec.Stack, &slice) + err = core.GetAllStackDependencies(ctx, database.Spec.Stack, &slice) if err != nil { return []reconcile.Request{} } diff --git a/internal/resources/ledgers/deployments.go b/internal/resources/ledgers/deployments.go index 2e6249c3..f843db23 100644 --- a/internal/resources/ledgers/deployments.go +++ b/internal/resources/ledgers/deployments.go @@ -342,7 +342,11 @@ func uninstallLedgerMonoWriterMultipleReader(ctx core.Context, stack *v1beta1.St func setCommonContainerConfiguration(ctx core.Context, stack *v1beta1.Stack, ledger *v1beta1.Ledger, imageConfiguration *registries.ImageConfiguration, database *v1beta1.Database, container *corev1.Container) error { env := make([]corev1.EnvVar, 0) - otlpEnv, err := settings.GetOTELEnvVars(ctx, stack.Name, core.LowerCamelCaseKind(ctx, ledger), " ") + serviceName, err := core.LowerCamelCaseKind(ctx, ledger) + if err != nil { + return err + } + otlpEnv, err := settings.GetOTELEnvVars(ctx, stack.Name, serviceName, " ") if err != nil { return err } diff --git a/internal/resources/orchestrations/deployments.go b/internal/resources/orchestrations/deployments.go index 633779ff..982b988d 100644 --- a/internal/resources/orchestrations/deployments.go +++ b/internal/resources/orchestrations/deployments.go @@ -57,7 +57,11 @@ func createDeployment( ) error { env := make([]corev1.EnvVar, 0) - otlpEnv, err := settings.GetOTELEnvVars(ctx, stack.Name, LowerCamelCaseKind(ctx, orchestration), " ") + serviceName, err := LowerCamelCaseKind(ctx, orchestration) + if err != nil { + return err + } + otlpEnv, err := settings.GetOTELEnvVars(ctx, stack.Name, serviceName, " ") if err != nil { return err } diff --git a/internal/resources/payments/deployments.go b/internal/resources/payments/deployments.go index 1cb54db6..ed8a310c 100644 --- a/internal/resources/payments/deployments.go +++ b/internal/resources/payments/deployments.go @@ -151,7 +151,11 @@ func temporalEnvVars(ctx core.Context, stack *v1beta1.Stack, payments *v1beta1.P func commonEnvVars(ctx core.Context, stack *v1beta1.Stack, payments *v1beta1.Payments, database *v1beta1.Database) ([]corev1.EnvVar, error) { env := make([]corev1.EnvVar, 0) - otlpEnv, err := settings.GetOTELEnvVars(ctx, stack.Name, core.LowerCamelCaseKind(ctx, payments), " ") + serviceName, err := core.LowerCamelCaseKind(ctx, payments) + if err != nil { + return nil, err + } + otlpEnv, err := settings.GetOTELEnvVars(ctx, stack.Name, serviceName, " ") if err != nil { return nil, err } diff --git a/internal/resources/reconciliations/deployments.go b/internal/resources/reconciliations/deployments.go index d635d41c..e1c171bf 100644 --- a/internal/resources/reconciliations/deployments.go +++ b/internal/resources/reconciliations/deployments.go @@ -25,7 +25,11 @@ func createDeployment( imageConfiguration *registries.ImageConfiguration, ) error { env := make([]v1.EnvVar, 0) - otlpEnv, err := settings.GetOTELEnvVars(ctx, stack.Name, core.LowerCamelCaseKind(ctx, reconciliation), " ") + serviceName, err := core.LowerCamelCaseKind(ctx, reconciliation) + if err != nil { + return err + } + otlpEnv, err := settings.GetOTELEnvVars(ctx, stack.Name, serviceName, " ") if err != nil { return err } diff --git a/internal/resources/stargates/deployment.go b/internal/resources/stargates/deployment.go index fb16a5b3..dd194525 100644 --- a/internal/resources/stargates/deployment.go +++ b/internal/resources/stargates/deployment.go @@ -17,7 +17,11 @@ func createDeployment(ctx core.Context, stack *v1beta1.Stack, stargate *v1beta1. env := make([]v1.EnvVar, 0) - otlpEnv, err := settings.GetOTELEnvVars(ctx, stack.Name, core.LowerCamelCaseKind(ctx, stargate), " ") + serviceName, err := core.LowerCamelCaseKind(ctx, stargate) + if err != nil { + return err + } + otlpEnv, err := settings.GetOTELEnvVars(ctx, stack.Name, serviceName, " ") if err != nil { return err } diff --git a/internal/resources/transactionplane/deployments.go b/internal/resources/transactionplane/deployments.go index c554a5ad..fd730d96 100644 --- a/internal/resources/transactionplane/deployments.go +++ b/internal/resources/transactionplane/deployments.go @@ -50,7 +50,11 @@ func commonEnvVars( ) ([]corev1.EnvVar, error) { env := make([]corev1.EnvVar, 0) - otlpEnv, err := settings.GetOTELEnvVars(ctx, stack.Name, LowerCamelCaseKind(ctx, t), " ") + serviceName, err := LowerCamelCaseKind(ctx, t) + if err != nil { + return nil, err + } + otlpEnv, err := settings.GetOTELEnvVars(ctx, stack.Name, serviceName, " ") if err != nil { return nil, err } diff --git a/internal/resources/wallets/deployment.go b/internal/resources/wallets/deployment.go index 69ec6939..d20dbaea 100644 --- a/internal/resources/wallets/deployment.go +++ b/internal/resources/wallets/deployment.go @@ -18,7 +18,11 @@ import ( func createDeployment(ctx core.Context, stack *v1beta1.Stack, wallets *v1beta1.Wallets, authClient *v1beta1.AuthClient, version string) error { env := make([]v1.EnvVar, 0) - otlpEnv, err := settings.GetOTELEnvVars(ctx, stack.Name, core.LowerCamelCaseKind(ctx, wallets), " ") + serviceName, err := core.LowerCamelCaseKind(ctx, wallets) + if err != nil { + return err + } + otlpEnv, err := settings.GetOTELEnvVars(ctx, stack.Name, serviceName, " ") if err != nil { return err } diff --git a/internal/resources/webhooks/deployment.go b/internal/resources/webhooks/deployment.go index eea68bc1..2d8a209f 100644 --- a/internal/resources/webhooks/deployment.go +++ b/internal/resources/webhooks/deployment.go @@ -28,7 +28,11 @@ func deploymentEnvVars(ctx core.Context, stack *v1beta1.Stack, webhooks *v1beta1 } env := make([]v1.EnvVar, 0) - otlpEnv, err := settings.GetOTELEnvVars(ctx, stack.Name, core.LowerCamelCaseKind(ctx, webhooks), " ") + serviceName, err := core.LowerCamelCaseKind(ctx, webhooks) + if err != nil { + return nil, err + } + otlpEnv, err := settings.GetOTELEnvVars(ctx, stack.Name, serviceName, " ") if err != nil { return nil, err } From 7e13e837798c81898fb72d65cf22fbb6bc80cc5a Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:07:54 +0200 Subject: [PATCH 15/29] fix(core): make LowerCaseKind return an error instead of panicking Same rationale as LowerCamelCaseKind: kind-resolution failure must not crash the controller-manager. Also reuse the already-computed object name in gatewayhttpapis.Create instead of resolving the kind twice. --- internal/core/module.go | 6 +++--- internal/resources/gatewayhttpapis/create.go | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/internal/core/module.go b/internal/core/module.go index d26d1d5d..0ee4cb51 100644 --- a/internal/core/module.go +++ b/internal/core/module.go @@ -15,10 +15,10 @@ func LowerCamelCaseKind(ctx Context, ob client.Object) (string, error) { return strcase.ToLowerCamel(kinds[0].Kind), nil } -func LowerCaseKind(ctx Context, ob client.Object) string { +func LowerCaseKind(ctx Context, ob client.Object) (string, error) { kinds, _, err := ctx.GetScheme().ObjectKinds(ob) if err != nil { - panic(err) + return "", err } - return strings.ToLower(kinds[0].Kind) + return strings.ToLower(kinds[0].Kind), nil } diff --git a/internal/resources/gatewayhttpapis/create.go b/internal/resources/gatewayhttpapis/create.go index e843b823..9d9350a8 100644 --- a/internal/resources/gatewayhttpapis/create.go +++ b/internal/resources/gatewayhttpapis/create.go @@ -14,9 +14,12 @@ var defaultOptions = []option{ } func Create(ctx core.Context, owner v1beta1.Module, options ...option) error { - objectName := core.LowerCaseKind(ctx, owner) - _, _, err := core.CreateOrUpdate[*v1beta1.GatewayHTTPAPI](ctx, types.NamespacedName{ - Name: core.GetObjectName(owner.GetStack(), core.LowerCaseKind(ctx, owner)), + objectName, err := core.LowerCaseKind(ctx, owner) + if err != nil { + return err + } + _, _, err = core.CreateOrUpdate[*v1beta1.GatewayHTTPAPI](ctx, types.NamespacedName{ + Name: core.GetObjectName(owner.GetStack(), objectName), }, func(t *v1beta1.GatewayHTTPAPI) error { t.Spec = v1beta1.GatewayHTTPAPISpec{ From 8372265e2436d621f705630bbd4588b0fc575fac Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:09:28 +0200 Subject: [PATCH 16/29] fix(brokers): return error from GetPublisherEnvVars on unhandled mode An unknown broker mode in a Broker status would crash the whole controller-manager from any module deployment helper. The function now returns an error that the five callers propagate through their existing error paths, failing only the offending reconciliation. --- internal/resources/brokers/utils.go | 8 ++++---- internal/resources/ledgers/deployments.go | 7 ++++++- internal/resources/orchestrations/deployments.go | 7 ++++++- internal/resources/payments/deployments.go | 14 ++++++++++++-- internal/resources/transactionplane/deployments.go | 7 ++++++- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/internal/resources/brokers/utils.go b/internal/resources/brokers/utils.go index eded4d49..f61fe483 100644 --- a/internal/resources/brokers/utils.go +++ b/internal/resources/brokers/utils.go @@ -68,12 +68,12 @@ func GetBrokerEnvVars(ctx core.Context, brokerURI *v1beta1.URI, stackName, servi return ret, nil } -func GetPublisherEnvVars(stack *v1beta1.Stack, broker *v1beta1.Broker, service string) []v1.EnvVar { +func GetPublisherEnvVars(stack *v1beta1.Stack, broker *v1beta1.Broker, service string) ([]v1.EnvVar, error) { switch broker.Status.Mode { case v1beta1.ModeOneStreamByService: return []v1.EnvVar{ core.Env("PUBLISHER_TOPIC_MAPPING", "*:"+core.GetObjectName(stack.Name, service)), - } + }, nil case v1beta1.ModeOneStreamByStack: ret := []v1.EnvVar{ core.Env("PUBLISHER_TOPIC_MAPPING", fmt.Sprintf("*:%s.%s", stack.Name, service)), @@ -82,9 +82,9 @@ func GetPublisherEnvVars(stack *v1beta1.Stack, broker *v1beta1.Broker, service s if broker.Status.URI.Scheme == "nats" { ret = append(ret, core.Env("PUBLISHER_NATS_AUTO_PROVISION", "false")) } - return ret + return ret, nil default: - panic(fmt.Sprintf("mode '%s' not handled", broker.Status.Mode)) + return nil, fmt.Errorf("broker mode '%s' not handled", broker.Status.Mode) } } diff --git a/internal/resources/ledgers/deployments.go b/internal/resources/ledgers/deployments.go index f843db23..4163ab2d 100644 --- a/internal/resources/ledgers/deployments.go +++ b/internal/resources/ledgers/deployments.go @@ -117,7 +117,12 @@ func installLedgerStateless(ctx core.Context, stack *v1beta1.Stack, ledger *v1be } container.Env = append(container.Env, brokerEnvVar...) - container.Env = append(container.Env, brokers.GetPublisherEnvVars(stack, broker, "ledger")...) + + publisherEnvVars, err := brokers.GetPublisherEnvVars(stack, broker, "ledger") + if err != nil { + return err + } + container.Env = append(container.Env, publisherEnvVars...) } bulkMaxSize, err := settings.GetInt(ctx, stack.Name, "ledger", "api", "bulk-max-size") diff --git a/internal/resources/orchestrations/deployments.go b/internal/resources/orchestrations/deployments.go index 982b988d..930c6cca 100644 --- a/internal/resources/orchestrations/deployments.go +++ b/internal/resources/orchestrations/deployments.go @@ -162,7 +162,12 @@ func createDeployment( return err } env = append(env, brokerEnvVars...) - env = append(env, brokers.GetPublisherEnvVars(stack, broker, "orchestration")...) + + publisherEnvVars, err := brokers.GetPublisherEnvVars(stack, broker, "orchestration") + if err != nil { + return err + } + env = append(env, publisherEnvVars...) serviceAccountName, err := settings.GetAWSServiceAccount(ctx, stack.Name) if err != nil { diff --git a/internal/resources/payments/deployments.go b/internal/resources/payments/deployments.go index ed8a310c..bb0b3647 100644 --- a/internal/resources/payments/deployments.go +++ b/internal/resources/payments/deployments.go @@ -389,7 +389,12 @@ func v3EnvVars( } envVars = append(envVars, additionalEnv...) - envVars = append(envVars, brokers.GetPublisherEnvVars(stack, broker, "payments")...) + + additionalEnv, err = brokers.GetPublisherEnvVars(stack, broker, "payments") + if err != nil { + return + } + envVars = append(envVars, additionalEnv...) } hash, additionalEnv, err = temporalEnvVars(ctx, stack, payments) @@ -485,7 +490,12 @@ func createV2ConnectorsDeployment(ctx core.Context, stack *v1beta1.Stack, paymen } env = append(env, brokerEnvVar...) - env = append(env, brokers.GetPublisherEnvVars(stack, broker, "payments")...) + + publisherEnvVars, err := brokers.GetPublisherEnvVars(stack, broker, "payments") + if err != nil { + return err + } + env = append(env, publisherEnvVars...) } serviceAccountName, err := settings.GetAWSServiceAccount(ctx, stack.Name) diff --git a/internal/resources/transactionplane/deployments.go b/internal/resources/transactionplane/deployments.go index fd730d96..ae2d9e94 100644 --- a/internal/resources/transactionplane/deployments.go +++ b/internal/resources/transactionplane/deployments.go @@ -102,7 +102,12 @@ func commonEnvVars( return nil, err } env = append(env, brokerEnvVars...) - env = append(env, brokers.GetPublisherEnvVars(stack, broker, "transactionplane")...) + + publisherEnvVars, err := brokers.GetPublisherEnvVars(stack, broker, "transactionplane") + if err != nil { + return nil, err + } + env = append(env, publisherEnvVars...) return env, nil } From f981f54494ddd4bd5848313ae25e4f6741d1a42a Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:10:13 +0200 Subject: [PATCH 17/29] fix(stacks): propagate module conversion error in setModulesCondition Converting a module's unstructured content to read its status would panic and crash the controller-manager on a single malformed module. The condition-building closure now returns the error (after marking the condition false) and the reconciliation fails normally. --- internal/resources/stacks/init.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/internal/resources/stacks/init.go b/internal/resources/stacks/init.go index 94fa0a35..b7fb0f55 100644 --- a/internal/resources/stacks/init.go +++ b/internal/resources/stacks/init.go @@ -86,7 +86,7 @@ func setModulesCondition(ctx Context, stack *v1beta1.Stack) error { return errors.New("multiple modules found") } - func() { + if err := func() error { condition := v1beta1.NewCondition(ModuleReconciliation, stack.Generation). SetReason(gvk.Kind) defer func() { @@ -103,29 +103,33 @@ func setModulesCondition(ctx Context, stack *v1beta1.Stack) error { module := AnyModule{} if err := runtime.DefaultUnstructuredConverter.FromUnstructured(l.Items[0].UnstructuredContent(), &module); err != nil { - panic(err) + condition.SetStatus(metav1.ConditionFalse).SetMessage("Unable to read module status") + return errors.Wrapf(err, "converting module %s to structured object", gvk.Kind) } stackReconcileCondition := module.Status.Conditions.Get("ReconciledWithStack") if stackReconcileCondition == nil { condition.SetStatus(metav1.ConditionFalse).SetMessage("Module not yet reconciled") - return + return nil } if stackReconcileCondition.Status != metav1.ConditionTrue { condition.SetStatus(metav1.ConditionFalse).SetMessage("Module not declared as reconciled for stack") - return + return nil } if stackReconcileCondition.Reason == "Spec" && stack.MustSkip() { condition.SetStatus(metav1.ConditionFalse).SetMessage("Module should be skipped but is not") - return + return nil } if stackReconcileCondition.Reason == "Skipped" && !stack.MustSkip() { condition.SetStatus(metav1.ConditionFalse).SetMessage("Module is skipped but should not") - return + return nil } condition.SetMessage("All checks passed") - }() + return nil + }(); err != nil { + return err + } } modules := make([]string, 0) From e6da8c5141ff545a838873f61b374326c75e85cb Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:10:45 +0200 Subject: [PATCH 18/29] fix(resourcereferences): log and drop event instead of panicking in watchResource A kind-resolution error in the watch map function would crash the whole controller-manager. Log the error and drop the event instead. Also hoist the loop-invariant GVK lookup out of the owner-references loop. --- internal/resources/resourcereferences/init.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/internal/resources/resourcereferences/init.go b/internal/resources/resourcereferences/init.go index b2ea1865..9f77b8a0 100644 --- a/internal/resources/resourcereferences/init.go +++ b/internal/resources/resourcereferences/init.go @@ -16,6 +16,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" collectionutils "github.com/formancehq/go-libs/v5/pkg/types/collections" @@ -42,14 +43,16 @@ func init() { func watchResource[T client.Object](ctx core.Context, object T) []reconcile.Request { ret := make([]reconcile.Request, 0) + gvk, err := apiutil.GVKForObject(&v1beta1.ResourceReference{}, ctx.GetScheme()) + if err != nil { + log.FromContext(ctx).Error(err, "resolving ResourceReference kind, dropping event") + return ret + } + apiVersion, kind := gvk.ToAPIVersionAndKind() + // Watch resources created by the ResourceReference var resourceReference string for _, reference := range object.GetOwnerReferences() { - gvk, err := apiutil.GVKForObject(&v1beta1.ResourceReference{}, ctx.GetScheme()) - if err != nil { - panic(err) - } - apiVersion, kind := gvk.ToAPIVersionAndKind() if reference.Kind == kind && reference.APIVersion == apiVersion { resourceReference = reference.Name break From 118e9023d2aac6d2d06506b785107a4f4151e33c Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:11:17 +0200 Subject: [PATCH 19/29] fix(auths): return error from HashFromHash instead of panicking createDeployment already has an error path; a hashing failure now fails the single Auth reconciliation instead of crashing the controller-manager. --- internal/resources/auths/deployment.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/internal/resources/auths/deployment.go b/internal/resources/auths/deployment.go index be03e314..c9bdff80 100644 --- a/internal/resources/auths/deployment.go +++ b/internal/resources/auths/deployment.go @@ -22,14 +22,14 @@ import ( "github.com/formancehq/operator/v3/internal/resources/settings" ) -func HashFromHash(o ...string) string { +func HashFromHash(o ...string) (string, error) { digest := sha256.New() for _, h := range o { if err := json.NewEncoder(digest).Encode(h); err != nil { - panic(err) + return "", err } } - return base64.StdEncoding.EncodeToString(digest.Sum(nil)) + return base64.StdEncoding.EncodeToString(digest.Sum(nil)), nil } func createDeployment(ctx Context, stack *v1beta1.Stack, auth *v1beta1.Auth, database *v1beta1.Database, @@ -136,7 +136,11 @@ func createDeployment(ctx Context, stack *v1beta1.Stack, auth *v1beta1.Auth, dat } return acc }, hashList) - annotations["auth-clients-secrets"] = HashFromHash(hashList...) + authClientsSecretsHash, err := HashFromHash(hashList...) + if err != nil { + return err + } + annotations["auth-clients-secrets"] = authClientsSecretsHash for _, client := range clients { if client.Spec.SecretFromSecret != nil { env = append(env, AuthClientSecretToEnvVars(client)) From e7968680ee5edf858ea9b1c9a91c987bcd0b8e58 Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:12:16 +0200 Subject: [PATCH 20/29] fix(core): make HashFromConfigMaps return an error instead of panicking A hashing failure would crash the whole controller-manager from any of the three callers (benthosstreams, auths, caddy). The function now returns the error and callers propagate it through their existing error paths. --- internal/core/utils.go | 6 +++--- internal/resources/auths/deployment.go | 6 +++++- internal/resources/benthosstreams/init.go | 6 +++++- internal/resources/caddy/caddy.go | 6 +++++- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/internal/core/utils.go b/internal/core/utils.go index 456d7d5f..da5153ea 100644 --- a/internal/core/utils.go +++ b/internal/core/utils.go @@ -24,14 +24,14 @@ import ( "github.com/formancehq/go-libs/v5/pkg/types/pointer" ) -func HashFromConfigMaps(configMaps ...*corev1.ConfigMap) string { +func HashFromConfigMaps(configMaps ...*corev1.ConfigMap) (string, error) { digest := sha256.New() for _, configMap := range configMaps { if err := json.NewEncoder(digest).Encode(configMap.Data); err != nil { - panic(err) + return "", err } } - return base64.StdEncoding.EncodeToString(digest.Sum(nil)) + return base64.StdEncoding.EncodeToString(digest.Sum(nil)), nil } func HashFromResources(resources ...*unstructured.Unstructured) string { diff --git a/internal/resources/auths/deployment.go b/internal/resources/auths/deployment.go index c9bdff80..bdfc53da 100644 --- a/internal/resources/auths/deployment.go +++ b/internal/resources/auths/deployment.go @@ -34,8 +34,12 @@ func HashFromHash(o ...string) (string, error) { func createDeployment(ctx Context, stack *v1beta1.Stack, auth *v1beta1.Auth, database *v1beta1.Database, configMap *corev1.ConfigMap, imageConfiguration *registries.ImageConfiguration, version string, clients []*v1beta1.AuthClient) error { + configHash, err := HashFromConfigMaps(configMap) + if err != nil { + return err + } annotations := map[string]string{ - "config-hash": HashFromConfigMaps(configMap), + "config-hash": configHash, } env := make([]corev1.EnvVar, 0) diff --git a/internal/resources/benthosstreams/init.go b/internal/resources/benthosstreams/init.go index 7da5b6c4..adbcc0b9 100644 --- a/internal/resources/benthosstreams/init.go +++ b/internal/resources/benthosstreams/init.go @@ -57,7 +57,11 @@ func Reconcile(ctx Context, _ *v1beta1.Stack, stream *v1beta1.BenthosStream) err return err } - stream.Status.ConfigMapHash = HashFromConfigMaps(cm) + configMapHash, err := HashFromConfigMaps(cm) + if err != nil { + return err + } + stream.Status.ConfigMapHash = configMapHash return nil } diff --git a/internal/resources/caddy/caddy.go b/internal/resources/caddy/caddy.go index c61c9b7f..5561e122 100644 --- a/internal/resources/caddy/caddy.go +++ b/internal/resources/caddy/caddy.go @@ -52,8 +52,12 @@ func DeploymentTemplate( env = append(env, core.Env("OTEL_EXPORTER_OTLP_PROTOCOL", "$(OTEL_TRACES_EXPORTER_OTLP_MODE)")) } + caddyfileHash, err := core.HashFromConfigMaps(caddyfile) + if err != nil { + return nil, err + } t.Spec.Template.Annotations = collectionutils.MergeMaps(t.Spec.Template.Annotations, map[string]string{ - "caddyfile-hash": core.HashFromConfigMaps(caddyfile), + "caddyfile-hash": caddyfileHash, }) t.Spec.Template.Spec.Volumes = []v1.Volume{ volumeFromConfigMap("caddyfile", caddyfile), From e42733cfc427ca4d2bff44181fe6dda045cf2ea0 Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:13:04 +0200 Subject: [PATCH 21/29] fix(benthos): propagate builtin-template read errors instead of panicking The templates map was built in an immediately-invoked closure that panicked on embedded-FS read errors, crashing the controller-manager. Extract it into a buildTemplates helper that returns the error so the single Benthos reconciliation fails instead. --- internal/resources/benthos/controller.go | 53 ++++++++++++++---------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/internal/resources/benthos/controller.go b/internal/resources/benthos/controller.go index b5bd8a1c..eb1e85f7 100644 --- a/internal/resources/benthos/controller.go +++ b/internal/resources/benthos/controller.go @@ -132,6 +132,31 @@ func reconcileEnvFromResourceReferences(ctx Context, b *v1beta1.Benthos) (map[st } // We need to this controller and keep it focused on benthos +// buildTemplates merges the user-provided templates with the builtin ones +// shipped with the operator. +func buildTemplates(b *v1beta1.Benthos) (map[string]string, error) { + ret := b.Spec.Templates + if ret == nil { + ret = make(map[string]string) + } + + files, err := builtinTemplates.ReadDir("builtin-templates") + if err != nil { + return nil, errors.Wrap(err, "reading builtin templates directory") + } + + for _, file := range files { + data, err := builtinTemplates.ReadFile("builtin-templates/" + file.Name()) + if err != nil { + return nil, errors.Wrapf(err, "reading builtin template '%s'", file.Name()) + } + + ret[file.Name()] = string(data) + } + + return ret, nil +} + func createDeployment(ctx Context, stack *v1beta1.Stack, b *v1beta1.Benthos) error { serviceAccountName, err := settings.GetAWSServiceAccount(ctx, stack.Name) if err != nil { @@ -225,6 +250,11 @@ func createDeployment(ctx Context, stack *v1beta1.Stack, b *v1beta1.Benthos) err volumeMounts := make([]corev1.VolumeMount, 0) configMaps := make([]*corev1.ConfigMap, 0) + templates, err := buildTemplates(b) + if err != nil { + return err + } + for _, object := range []struct { discr string files map[string]string @@ -235,28 +265,7 @@ func createDeployment(ctx Context, stack *v1beta1.Stack, b *v1beta1.Benthos) err }, { discr: "templates", - files: func() map[string]string { - ret := b.Spec.Templates - if ret == nil { - ret = make(map[string]string) - } - - files, err := builtinTemplates.ReadDir("builtin-templates") - if err != nil { - panic(err) - } - - for _, file := range files { - data, err := builtinTemplates.ReadFile("builtin-templates/" + file.Name()) - if err != nil { - panic(err) - } - - ret[file.Name()] = string(data) - } - - return ret - }(), + files: templates, }, } { From 17fbb3521d591b94d28f7587fa7c75f5a286be20 Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:13:52 +0200 Subject: [PATCH 22/29] fix(kubectl-stacks): return marshal error in enable instead of panicking The function already returns an error; a CLI must report failures through its normal error path, not a panic stack trace. --- tools/kubectl-stacks/enable.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/kubectl-stacks/enable.go b/tools/kubectl-stacks/enable.go index 5ee2b6e1..7efd7494 100644 --- a/tools/kubectl-stacks/enable.go +++ b/tools/kubectl-stacks/enable.go @@ -33,7 +33,7 @@ func enable(cmd *cobra.Command, client *rest.RESTClient, name string) error { }, }) if err != nil { - panic(err) + return err } return client.Patch(types.MergePatchType). From 0ea83480c61797379ba784bb1143d115b4873f08 Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:14:12 +0200 Subject: [PATCH 23/29] fix(kubectl-stacks): return marshal error in disable instead of panicking Same rationale as enable: report failures through the command's normal error path. --- tools/kubectl-stacks/disable.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/kubectl-stacks/disable.go b/tools/kubectl-stacks/disable.go index bbeeda1d..1f469aab 100644 --- a/tools/kubectl-stacks/disable.go +++ b/tools/kubectl-stacks/disable.go @@ -33,7 +33,7 @@ func disable(cmd *cobra.Command, client *rest.RESTClient, name string) error { }, }) if err != nil { - panic(err) + return err } return client.Patch(types.MergePatchType). From aa8bff96b471c355d990e736ec38132f486b26a5 Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:14:30 +0200 Subject: [PATCH 24/29] fix(kubectl-stacks): return marshal error in setDebug instead of panicking Same rationale as enable: report failures through the command's normal error path. --- tools/kubectl-stacks/set-debug.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/kubectl-stacks/set-debug.go b/tools/kubectl-stacks/set-debug.go index c0869afd..7cf4ecff 100644 --- a/tools/kubectl-stacks/set-debug.go +++ b/tools/kubectl-stacks/set-debug.go @@ -33,7 +33,7 @@ func setDebug(cmd *cobra.Command, client *rest.RESTClient, name string, b bool) }, }) if err != nil { - panic(err) + return err } return client.Patch(types.MergePatchType). From e9eb6a7e5a335b670377ebd959f01d062ec7fa97 Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:14:56 +0200 Subject: [PATCH 25/29] fix(kubectl-stacks): return marshal error in lockStack instead of panicking Same rationale as enable: report failures through the command's normal error path. --- tools/kubectl-stacks/lock.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/kubectl-stacks/lock.go b/tools/kubectl-stacks/lock.go index 47a15a7b..f17ec107 100644 --- a/tools/kubectl-stacks/lock.go +++ b/tools/kubectl-stacks/lock.go @@ -61,7 +61,7 @@ func lockStack(cmd *cobra.Command, client *rest.RESTClient, name string) error { }, }) if err != nil { - panic(err) + return err } return client.Patch(types.MergePatchType). From 397b1f210d61c902b2caa6947bf33f383a3aac5e Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:15:17 +0200 Subject: [PATCH 26/29] fix(kubectl-stacks): return marshal error in unlockStack instead of panicking Same rationale as enable: report failures through the command's normal error path. --- tools/kubectl-stacks/unlock.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/kubectl-stacks/unlock.go b/tools/kubectl-stacks/unlock.go index 042f4d16..2f093845 100644 --- a/tools/kubectl-stacks/unlock.go +++ b/tools/kubectl-stacks/unlock.go @@ -75,7 +75,7 @@ func unlockStack(cmd *cobra.Command, client *rest.RESTClient, name string) error }, }) if err != nil { - panic(err) + return err } return client.Patch(types.MergePatchType). From 284c3814f79490824ad0003de09e1e9a266435a9 Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:15:49 +0200 Subject: [PATCH 27/29] fix(kubectl-stacks): join deferred unlock error in upgrade instead of panicking A failure to unlock stacks after an upgrade is exactly the situation the user must be told about cleanly; join it with the command error instead of panicking past the cobra error handling. --- tools/kubectl-stacks/upgrade.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/kubectl-stacks/upgrade.go b/tools/kubectl-stacks/upgrade.go index 12f77adb..2543b3fb 100644 --- a/tools/kubectl-stacks/upgrade.go +++ b/tools/kubectl-stacks/upgrade.go @@ -1,6 +1,8 @@ package main import ( + "errors" + "fmt" "time" "github.com/pterm/pterm" @@ -34,14 +36,14 @@ func NewUpgradeCommand(configFlags *genericclioptions.ConfigFlags) *cobra.Comman return ret } -func upgrade(cmd *cobra.Command, client *rest.RESTClient) error { +func upgrade(cmd *cobra.Command, client *rest.RESTClient) (returnErr error) { stackList, err := lockAllStacks(cmd, client) if err != nil { return err } defer func() { if err := unlockAllStacks(cmd, client); err != nil { - panic(err) + returnErr = errors.Join(returnErr, fmt.Errorf("unlocking stacks: %w", err)) } }() for _, stack := range stackList.Items { From b46b09171272f3ecfc15a3a78f1561a097cd1b02 Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 22:16:15 +0200 Subject: [PATCH 28/29] fix(kubectl-stacks): move scheme registration from init to main Register the scheme in main() and exit with a printed error instead of panicking from init(), keeping all CLI failures on the same stderr+exit-code path. --- tools/kubectl-stacks/main.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tools/kubectl-stacks/main.go b/tools/kubectl-stacks/main.go index a078af91..abb79f77 100644 --- a/tools/kubectl-stacks/main.go +++ b/tools/kubectl-stacks/main.go @@ -51,15 +51,14 @@ func main() { flags := pflag.NewFlagSet("kubectl-stacks", pflag.ExitOnError) pflag.CommandLine = flags - root := NewRootCommand() - if err := root.Execute(); err != nil { + if err := v1beta1.AddToScheme(scheme.Scheme); err != nil { _, _ = fmt.Fprintln(os.Stderr, err) os.Exit(1) } -} -func init() { - if err := v1beta1.AddToScheme(scheme.Scheme); err != nil { - panic(err) + root := NewRootCommand() + if err := root.Execute(); err != nil { + _, _ = fmt.Fprintln(os.Stderr, err) + os.Exit(1) } } From 6b68f1fc55397da5028819971ed901da19d1ea49 Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Tue, 9 Jun 2026 23:15:47 +0200 Subject: [PATCH 29/29] chore: refresh settings catalog --- .../settings.catalog.json | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/09-Configuration reference/settings.catalog.json b/docs/09-Configuration reference/settings.catalog.json index a4316ca8..52cc2d40 100644 --- a/docs/09-Configuration reference/settings.catalog.json +++ b/docs/09-Configuration reference/settings.catalog.json @@ -12,7 +12,7 @@ "key": "auth.issuers", "valueType": "string", "sources": [ - "internal/resources/auths/deployment.go:69", + "internal/resources/auths/deployment.go:77", "internal/resources/auths/env.go:34" ] }, @@ -366,7 +366,7 @@ "key": "ledger.api.bulk-max-size", "valueType": "int", "sources": [ - "internal/resources/ledgers/deployments.go:123" + "internal/resources/ledgers/deployments.go:128" ] }, { @@ -387,8 +387,8 @@ "key": "ledger.experimental-exporters", "valueType": "bool", "sources": [ - "internal/resources/ledgers/deployments.go:149", - "internal/resources/ledgers/deployments.go:264" + "internal/resources/ledgers/deployments.go:154", + "internal/resources/ledgers/deployments.go:269" ] }, { @@ -416,8 +416,8 @@ "key": "ledger.schema-enforcement-mode", "valueType": "string", "sources": [ - "internal/resources/ledgers/deployments.go:131", - "internal/resources/ledgers/deployments.go:239" + "internal/resources/ledgers/deployments.go:136", + "internal/resources/ledgers/deployments.go:244" ] }, { @@ -435,7 +435,7 @@ } ], "sources": [ - "internal/resources/ledgers/deployments.go:209" + "internal/resources/ledgers/deployments.go:214" ] }, { @@ -453,7 +453,7 @@ } ], "sources": [ - "internal/resources/ledgers/deployments.go:248" + "internal/resources/ledgers/deployments.go:253" ] }, { @@ -479,7 +479,7 @@ } ], "sources": [ - "internal/resources/ledgers/deployments.go:221" + "internal/resources/ledgers/deployments.go:226" ] }, { @@ -519,14 +519,14 @@ "key": "namespace.annotations", "valueType": "map[string]string", "sources": [ - "internal/resources/stacks/init.go:173" + "internal/resources/stacks/init.go:177" ] }, { "key": "namespace.labels", "valueType": "map[string]string", "sources": [ - "internal/resources/stacks/init.go:154" + "internal/resources/stacks/init.go:158" ] }, { @@ -562,7 +562,7 @@ "valueType": "int", "default": "10", "sources": [ - "internal/resources/orchestrations/deployments.go:173" + "internal/resources/orchestrations/deployments.go:182" ] }, { @@ -650,7 +650,7 @@ "key": "temporal.dsn", "valueType": "uri", "sources": [ - "internal/resources/orchestrations/deployments.go:80", + "internal/resources/orchestrations/deployments.go:84", "internal/resources/payments/deployments.go:43" ] }, @@ -658,7 +658,7 @@ "key": "temporal.tls.crt", "valueType": "string", "sources": [ - "internal/resources/orchestrations/deployments.go:124", + "internal/resources/orchestrations/deployments.go:128", "internal/resources/payments/deployments.go:81" ] }, @@ -666,7 +666,7 @@ "key": "temporal.tls.key", "valueType": "string", "sources": [ - "internal/resources/orchestrations/deployments.go:129", + "internal/resources/orchestrations/deployments.go:133", "internal/resources/payments/deployments.go:89" ] }, @@ -675,7 +675,7 @@ "valueType": "bool", "default": "false", "sources": [ - "internal/resources/transactionplane/deployments.go:125" + "internal/resources/transactionplane/deployments.go:134" ] } ]