Skip to content

Conversation

@sylr
Copy link
Contributor

@sylr sylr commented Nov 5, 2025

No description provided.

Signed-off-by: Sylvain Rabot <sylvain@formance.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 5, 2025

Walkthrough

Adds ValueFrom to SettingsSpec to allow values sourced from Secrets or ConfigMaps, makes Value optional, updates deep-copy methods, adds resolution logic with namespace fallback and optional handling, and updates controller-gen version.

Changes

Cohort / File(s) Summary
Build Configuration
Justfile
Updated controller-gen version from v0.14.0 to v0.19.0 in manifests and generate targets
Type Definitions
api/formance.com/v1beta1/settings_types.go
Made Value optional (json:"value,omitempty"); added ValueFrom *ValueFrom to SettingsSpec; added ValueFrom type with SecretKeyRef and ConfigMapKeyRef fields (corev1 selectors)
Generated Deep Copy Methods
api/formance.com/v1beta1/zz_generated.deepcopy.go
Added DeepCopyInto and DeepCopy for ValueFrom; updated SettingsSpec.DeepCopyInto to deep-copy ValueFrom
Settings Resolution Logic
internal/resources/settings/helpers.go
Added ErrResourceNotFound, ErrOptionalResourceNotFound; changed findMatchingSettings signature to accept ctx and stack; added resolveValueFrom(ctx, namespace, valueFrom) to fetch Secret/ConfigMap with optional handling and fallback to formance-system; updated callers and error wrapping
Tests
internal/resources/settings/helpers_test.go
Added TestFindMatchingSettingsWithValueFrom covering secrets, configmaps, optional resources, binary data, namespace fallback and error cases; updated tests to use context; added mockManager to support controller-runtime client in tests

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant findMatchingSettings
    participant resolveValueFrom
    participant K8sClient
    participant StackNS
    participant FormanceNS

    Caller->>findMatchingSettings: Request setting (ctx, stack, key)
    alt Setting has inline Value
        findMatchingSettings-->>Caller: return Value
    else Setting has ValueFrom
        findMatchingSettings->>resolveValueFrom: resolve (ctx, stackNS, valueFrom)
        resolveValueFrom->>K8sClient: Get resource (Secret/ConfigMap) in StackNS
        alt resource found
            K8sClient-->>resolveValueFrom: data
            resolveValueFrom-->>findMatchingSettings: return value
        else resource not found (and required)
            resolveValueFrom->>K8sClient: Get resource in FormanceNS (fallback)
            K8sClient-->>resolveValueFrom: data or error
            resolveValueFrom-->>findMatchingSettings: return value or error
        else resource optional and not found
            resolveValueFrom-->>findMatchingSettings: indicate optional-not-found
        end
        findMatchingSettings-->>Caller: resolved value or error
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇
In burrows deep a rabbit grins,
ValueFrom fetches secrets in bins,
From stack to system it will peep,
Optional whispers, fallbacks leap,
Hops of code make settings neat.

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No description was provided by the author, making it impossible to evaluate relevance to the changeset. Add a pull request description explaining the purpose and motivation for adding the valueFrom field to Settings.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding a valueFrom field to the Settings type as documented in the API changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch settings-valuefrom

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sylr sylr marked this pull request as ready for review November 6, 2025 17:47
@sylr sylr requested a review from a team as a code owner November 6, 2025 17:47
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b431a41 and c37a6f9.

⛔ Files ignored due to path filters (24)
  • config/crd/bases/formance.com_authclients.yaml is excluded by !**/*.yaml
  • config/crd/bases/formance.com_auths.yaml is excluded by !**/*.yaml
  • config/crd/bases/formance.com_benthos.yaml is excluded by !**/*.yaml
  • config/crd/bases/formance.com_benthosstreams.yaml is excluded by !**/*.yaml
  • config/crd/bases/formance.com_brokerconsumers.yaml is excluded by !**/*.yaml
  • config/crd/bases/formance.com_brokers.yaml is excluded by !**/*.yaml
  • config/crd/bases/formance.com_brokertopics.yaml is excluded by !**/*.yaml
  • config/crd/bases/formance.com_databases.yaml is excluded by !**/*.yaml
  • config/crd/bases/formance.com_gatewayhttpapis.yaml is excluded by !**/*.yaml
  • config/crd/bases/formance.com_gateways.yaml is excluded by !**/*.yaml
  • config/crd/bases/formance.com_ledgers.yaml is excluded by !**/*.yaml
  • config/crd/bases/formance.com_orchestrations.yaml is excluded by !**/*.yaml
  • config/crd/bases/formance.com_payments.yaml is excluded by !**/*.yaml
  • config/crd/bases/formance.com_reconciliations.yaml is excluded by !**/*.yaml
  • config/crd/bases/formance.com_resourcereferences.yaml is excluded by !**/*.yaml
  • config/crd/bases/formance.com_searches.yaml is excluded by !**/*.yaml
  • config/crd/bases/formance.com_settings.yaml is excluded by !**/*.yaml
  • config/crd/bases/formance.com_stacks.yaml is excluded by !**/*.yaml
  • config/crd/bases/formance.com_stargates.yaml is excluded by !**/*.yaml
  • config/crd/bases/formance.com_versions.yaml is excluded by !**/*.yaml
  • config/crd/bases/formance.com_wallets.yaml is excluded by !**/*.yaml
  • config/crd/bases/formance.com_webhooks.yaml is excluded by !**/*.yaml
  • config/rbac/role.yaml is excluded by !**/*.yaml
  • go.mod is excluded by !**/*.mod
📒 Files selected for processing (5)
  • Justfile (1 hunks)
  • api/formance.com/v1beta1/settings_types.go (2 hunks)
  • api/formance.com/v1beta1/zz_generated.deepcopy.go (2 hunks)
  • internal/resources/settings/helpers.go (4 hunks)
  • internal/resources/settings/helpers_test.go (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
internal/resources/settings/helpers_test.go (4)
internal/core/context.go (2)
  • NewContext (41-46)
  • Context (10-16)
api/formance.com/v1beta1/settings_types.go (3)
  • Settings (272-277)
  • SettingsSpec (25-39)
  • ValueFrom (42-49)
internal/core/platform.go (1)
  • Platform (3-13)
internal/resources/settings/helpers.go (1)
  • SplitKeywordWithDot (518-537)
api/formance.com/v1beta1/zz_generated.deepcopy.go (1)
api/formance.com/v1beta1/settings_types.go (1)
  • ValueFrom (42-49)
internal/resources/settings/helpers.go (2)
internal/core/context.go (1)
  • Context (10-16)
api/formance.com/v1beta1/settings_types.go (3)
  • SettingsList (290-294)
  • Settings (272-277)
  • ValueFrom (42-49)

Comment on lines +528 to +570
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

// Build list of objects for fake client
objects := make([]client.Object, 0)
for _, secret := range tt.secrets {
objects = append(objects, secret)
}
for _, cm := range tt.configMaps {
objects = append(objects, cm)
}

fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(objects...).
Build()

// Create a mock manager
mockMgr := &mockManager{
client: fakeClient,
scheme: scheme,
}

coreMgr := core.NewDefaultManager(mockMgr, core.Platform{
Region: "test",
Environment: "test",
})

ctx := core.NewContext(coreMgr, context.Background())

value, err := findMatchingSettings(ctx, tt.stack, []v1beta1.Settings{tt.setting}, SplitKeywordWithDot(tt.setting.Spec.Key)...)

if tt.expectedError != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tt.expectedError)
require.Nil(t, value)
} else {
require.NoError(t, err)
require.NotNil(t, value)
require.Equal(t, tt.expectedResult, *value)
}
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix loop variable capture in parallel subtests.

Each closure shares the same loop variable tt; with t.Parallel() every subtest runs after the loop completes and only sees the final test case. Rebind tt inside the loop so each subtest operates on its own data.

-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Build list of objects for fake client
objects := make([]client.Object, 0)
for _, secret := range tt.secrets {
objects = append(objects, secret)
}
for _, cm := range tt.configMaps {
objects = append(objects, cm)
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(objects...).
Build()
// Create a mock manager
mockMgr := &mockManager{
client: fakeClient,
scheme: scheme,
}
coreMgr := core.NewDefaultManager(mockMgr, core.Platform{
Region: "test",
Environment: "test",
})
ctx := core.NewContext(coreMgr, context.Background())
value, err := findMatchingSettings(ctx, tt.stack, []v1beta1.Settings{tt.setting}, SplitKeywordWithDot(tt.setting.Spec.Key)...)
if tt.expectedError != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tt.expectedError)
require.Nil(t, value)
} else {
require.NoError(t, err)
require.NotNil(t, value)
require.Equal(t, tt.expectedResult, *value)
}
})
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Build list of objects for fake client
objects := make([]client.Object, 0)
for _, secret := range tt.secrets {
objects = append(objects, secret)
}
for _, cm := range tt.configMaps {
objects = append(objects, cm)
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(objects...).
Build()
// Create a mock manager
mockMgr := &mockManager{
client: fakeClient,
scheme: scheme,
}
coreMgr := core.NewDefaultManager(mockMgr, core.Platform{
Region: "test",
Environment: "test",
})
ctx := core.NewContext(coreMgr, context.Background())
value, err := findMatchingSettings(ctx, tt.stack, []v1beta1.Settings{tt.setting}, SplitKeywordWithDot(tt.setting.Spec.Key)...)
if tt.expectedError != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tt.expectedError)
require.Nil(t, value)
} else {
require.NoError(t, err)
require.NotNil(t, value)
require.Equal(t, tt.expectedResult, *value)
}
})
}
🤖 Prompt for AI Agents
internal/resources/settings/helpers_test.go around lines 528 to 570: the
subtests call t.Parallel() while closing over the loop variable tt, causing all
subtests to share the same tt value; rebind the loop variable at the start of
each iteration (e.g., local := tt) and use that local variable inside the t.Run
closure so each parallel subtest captures its own copy of the test case and test
data.

}
if setting.Spec.ValueFrom != nil {
// Resolve value from secret or configmap
value, err := resolveValueFrom(ctx, stack, setting.Spec.ValueFrom)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure referencing a secret from a stack namespace is a good idea.
You know, chicken and eggs problems.
But, I guess, the operator will eventually reconcile at the end.

if err != nil {
if errors.Is(err, ErrResourceNotFound) || errors.Is(err, ErrOptionalResourceNotFound) {
// Try fallback to formance-system namespace
value, err2 := resolveValueFrom(ctx, "formance-system", setting.Spec.ValueFrom)
Copy link
Contributor

@gfyrag gfyrag Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you can get the current namespace of the running pod? By injecting it using the downward api.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would you do ?

Copy link
Contributor

@gfyrag gfyrag Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you can get the current namespace of the running pod? By injecting it using the downward api.

- Fix bug where optional resource check used wrong error variable (err vs err2)
- Simplify code since CEL validation ensures exactly one of value/valueFrom is set
- Remove test case for impossible state (both value and valueFrom set)
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @internal/resources/settings/helpers.go:
- Line 33: The error message returned from the helper that wraps the underlying
error currently says "listings settings" which is a typo; update the wrapped
error string to "listing settings" (i.e., change fmt.Errorf("listings settings:
%w", err) to fmt.Errorf("listing settings: %w", err)) so the returned error
reads correctly; locate this change in the helper in
internal/resources/settings/helpers.go where that fmt.Errorf is used.
- Line 41: Replace the typo in the error string in the fmt.Errorf call that
currently reads fmt.Errorf("listings settings: %w", err) so it matches the
correct wording used elsewhere (e.g., change "listings settings" to "listing
settings" or the project's canonical phrase); update the string inside the
fmt.Errorf call accordingly where that return nil, fmt.Errorf(...) occurs.
🧹 Nitpick comments (2)
internal/resources/settings/helpers_test.go (1)

349-368: Consider adding test for optional key missing in existing secret.

The test covers optional secret not found, but doesn't test the case where the secret exists but the key is missing with Optional: true. Based on the implementation in helpers.go (lines 457-459), this should also return ErrOptionalResourceNotFound. Consider adding a test case for this scenario.

internal/resources/settings/helpers.go (1)

402-424: Consider defensive handling for optional resources in fallback scenario.

When an optional resource is not found in the stack namespace, the fallback to formance-system should also treat it as optional. Currently, the code only returns an empty string if err2 is ErrOptionalResourceNotFound (line 415-418). However, since the same valueFrom object is passed to both resolveValueFrom calls, if the primary call returned ErrOptionalResourceNotFound, the fallback should inherit that "optional" context regardless of the error type returned by the second call.

Consider checking whether the primary error indicated an optional resource:

if errors.Is(err, ErrOptionalResourceNotFound) || errors.Is(err2, ErrOptionalResourceNotFound) {
    empty := ""
    return &empty, nil
}

This makes the intent explicit and defensive against unexpected error type variations.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c37a6f9 and ba49323.

📒 Files selected for processing (2)
  • internal/resources/settings/helpers.go
  • internal/resources/settings/helpers_test.go
🧰 Additional context used
🧬 Code graph analysis (2)
internal/resources/settings/helpers.go (4)
internal/resources/settings/settings.go (1)
  • New (25-36)
internal/core/context.go (1)
  • Context (10-16)
api/formance.com/v1beta1/settings_types.go (3)
  • SettingsList (290-294)
  • Settings (272-277)
  • ValueFrom (42-49)
internal/tests/internal/bootstrap.go (1)
  • List (219-221)
internal/resources/settings/helpers_test.go (6)
internal/core/context.go (2)
  • NewContext (41-46)
  • Context (10-16)
api/formance.com/v1beta1/settings_types.go (3)
  • Settings (272-277)
  • SettingsSpec (25-39)
  • ValueFrom (42-49)
api/formance.com/v1beta1/groupversion_info.go (1)
  • AddToScheme (44-44)
api/formance.com/v1beta1/shared.go (1)
  • Object (294-300)
internal/core/platform.go (1)
  • Platform (3-13)
internal/resources/settings/helpers.go (1)
  • SplitKeywordWithDot (515-534)
🔇 Additional comments (6)
internal/resources/settings/helpers_test.go (3)

3-28: LGTM on imports.

The new imports are appropriate for the fake client testing setup with controller-runtime.


210-224: LGTM on context passing.

Passing nil for the manager is acceptable here since these tests only exercise the key-matching logic with direct Value settings, not ValueFrom resolution.


546-580: Mock implementation is adequate for test purposes.

The mockManager correctly implements the Manager interface for controller-runtime v0.17.1. The no-op implementations for unused methods, including SetFields, are appropriate for testing.

internal/resources/settings/helpers.go (3)

22-25: LGTM on sentinel errors.

Well-defined sentinel errors enable clean error type checking with errors.Is() for the fallback logic.


433-500: Well-structured ValueFrom resolution logic.

The implementation correctly handles:

  • Secret and ConfigMap lookups with proper namespace handling
  • Optional resource and key scenarios
  • ConfigMap BinaryData fallback
  • Descriptive error messages with namespace context

393-393: All call sites of findMatchingSettings have been correctly updated.

The function signature change to include ctx core.Context and stack string parameters has been fully implemented across all three call sites:

  • Internal call (line 44): Properly passes both ctx and stack parameters
  • Test calls (lines 210, 531): Both pass appropriately constructed or derived ctx and stack values

"keylen": fmt.Sprint(len(keys)),
}); err != nil {
return nil, errors.Wrap(err, "listings settings")
return nil, fmt.Errorf("listings settings: %w", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor typo in error message.

"listings settings" should be "listing settings".

📝 Proposed fix
-		return nil, fmt.Errorf("listings settings: %w", err)
+		return nil, fmt.Errorf("listing settings: %w", err)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return nil, fmt.Errorf("listings settings: %w", err)
return nil, fmt.Errorf("listing settings: %w", err)
🤖 Prompt for AI Agents
In @internal/resources/settings/helpers.go at line 33, The error message
returned from the helper that wraps the underlying error currently says
"listings settings" which is a typo; update the wrapped error string to "listing
settings" (i.e., change fmt.Errorf("listings settings: %w", err) to
fmt.Errorf("listing settings: %w", err)) so the returned error reads correctly;
locate this change in the helper in internal/resources/settings/helpers.go where
that fmt.Errorf is used.

"keylen": fmt.Sprint(len(keys)),
}); err != nil {
return nil, errors.Wrap(err, "listings settings")
return nil, fmt.Errorf("listings settings: %w", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same typo in second error message.

📝 Proposed fix
-		return nil, fmt.Errorf("listings settings: %w", err)
+		return nil, fmt.Errorf("listing settings: %w", err)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return nil, fmt.Errorf("listings settings: %w", err)
return nil, fmt.Errorf("listing settings: %w", err)
🤖 Prompt for AI Agents
In @internal/resources/settings/helpers.go at line 41, Replace the typo in the
error string in the fmt.Errorf call that currently reads fmt.Errorf("listings
settings: %w", err) so it matches the correct wording used elsewhere (e.g.,
change "listings settings" to "listing settings" or the project's canonical
phrase); update the string inside the fmt.Errorf call accordingly where that
return nil, fmt.Errorf(...) occurs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants