Skip to content

feat(connectivity): add Connectivity module bound to the stack ledger - #492

Open
Dav-14 wants to merge 17 commits into
mainfrom
feat/connectivity-module
Open

feat(connectivity): add Connectivity module bound to the stack ledger#492
Dav-14 wants to merge 17 commits into
mainfrom
feat/connectivity-module

Conversation

@Dav-14

@Dav-14 Dav-14 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

What

Adds a new formance.com/v1beta1 Connectivity stack module that binds connectivity to the stack's ledger, mirroring the Ledger v3 delegation pattern.

The module doesn't run the workload itself — it delegates to the connectivity operator (connectivity.formance.com) and reflects its readiness, exactly like the Ledger module delegates to the ledger operator (ledger.formance.com Cluster).

Behaviour

  1. Capability + API-group detection (same mechanism as Ledger v3): at controller start-up it lists CRDs, checks the connectivity.formance.com/Connectivity CRD is present with a served version, and runs a SelfSubjectAccessReview for each required verb. If the group/CRD/RBAC is missing it reports the capability as unavailable and stays pending — it never fails controller setup.

  2. Gates on Ledger v3: connectivity ingests into the Ledger v3 gRPC endpoint, so it only provisions once the stack's Ledger module is v3 and ready.

  3. Binds to the stack ledger: provisions a connectivity.formance.com/v1alpha1 Connectivity with

    • ledgerAddress = the ledger v3 gRPC service, and
    • ledgerTLS = the ledger backend TLS secret (CA + SNI).

    The connection details come from ledgers.V3GRPCBackendRef — the single source of truth already used to reach the ledger over gRPC — so connectivity and the gateway stay in sync (no duplicated address/secret naming).

  4. Reflects the delegated resource's status.phase back onto the module's Ready condition.

Files

  • api/formance.com/v1beta1/connectivity_types.go — new module CR (Connectivity, labelled formance.com/kind=module).
  • internal/resources/connectivities/init.go — reconciler, capability detection, ledger-v3 gate, delegated-resource bind.
  • internal/resources/connectivities/init_test.go — unit tests for the capability-gating paths (discovery failure, inaccessible resource, missing RBAC, capability-unavailable reconcile).
  • internal/resources/ledgers/exports.go — exports IsV3 + V3GRPCBackendRef so the ledger v3 gRPC connection stays the single source of truth.
  • internal/resources/all.go — registers the module.
  • Generated: CRD base, RBAC role, deepcopy, helm CRD + operator ClusterRole.

Test

  • go build ./...
  • go vet ./internal/resources/connectivities/...
  • go test ./internal/resources/connectivities/...
  • helm template ./helm/crds renders ✅

Follow-ups (out of scope)

  • Wiring the connectivity image / registry rewrite + imagePullSecrets from stack settings (currently left to the connectivity operator defaults).
  • Membership/fctl surface to enable the module on a stack.

@Dav-14
Dav-14 requested a review from a team as a code owner July 16, 2026 14:12
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This change adds the v1beta1 Connectivity resource, delegated reconciliation through Ledger v3, Gateway routing, capability detection, credential and monitoring handling, network policies, tests, and module documentation.

Changes

Connectivity module

Layer / File(s) Summary
Connectivity API contracts
api/formance.com/v1beta1/connectivity_types.go, api/formance.com/v1beta1/zz_generated.deepcopy.go, docs/09-Configuration reference/02-Custom Resource Definitions.md
Defines the Connectivity resource, status helpers, list type, scheme registration, deepcopy methods, and CRD reference.
Capability detection and controller wiring
internal/resources/connectivities/init.go, internal/resources/all.go, internal/resources/connectivities/init_test.go, docs/10-Development/01-Adding a module.md
Adds capability checks, conditional watches, reconciler registration, and module integration guidance.
Delegated Connectivity reconciliation
internal/resources/connectivities/init.go, internal/resources/ledgers/exports.go, internal/resources/connectivities/init_test.go
Gates reconciliation on Ledger and credential readiness, provisions delegated resources, applies monitoring and authentication, creates Gateway routing, resolves backend ports, and mirrors status.
Gateway TLS retry handling
internal/resources/gateways/deployment.go, internal/resources/gateways/deployment_test.go
Returns pending errors for missing backend TLS Secrets and preserves hard errors for other retrieval failures.
Connectivity network access
internal/resources/stacks/networkpolicies.go, internal/resources/stacks/networkpolicies_test.go, internal/tests/networkpolicy_controller_test.go
Adds and validates same-namespace Connectivity access to Ledger v3 pods and policy deletion when network policies are disabled.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ConnectivityResource
  participant ConnectivityReconciler
  participant Ledger
  participant DelegatedConnectivity
  participant Gateway
  ConnectivityResource->>ConnectivityReconciler: Provide module configuration
  ConnectivityReconciler->>Ledger: Check readiness and credentials
  Ledger-->>ConnectivityReconciler: Return v3 backend and credential references
  ConnectivityReconciler->>DelegatedConnectivity: Apply delegated Connectivity resource
  DelegatedConnectivity-->>ConnectivityReconciler: Return phase and readiness
  ConnectivityReconciler->>Gateway: Apply secured API route
  ConnectivityReconciler-->>ConnectivityResource: Set status conditions
Loading

Possibly related PRs

Suggested reviewers: gfyrag

Poem

A rabbit checks the Ledger gate,
Credentials make the route translate.
Conditions mark the module bright,
Network paths permit the flight.
Connectivity joins the stack tonight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.60% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the new Connectivity module and its binding to the stack Ledger.
Description check ✅ Passed The description directly explains the Connectivity module, delegation behavior, Ledger binding, readiness handling, tests, and related changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/connectivity-module

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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.

@NumaryBot

NumaryBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🛑 Changes requested — automated review

The Connectivity module introduces several correctness and reliability gaps that must be addressed before merging. The most critical are: (1) the reconciler registers no watch on the ledger Credentials object it creates, so it can stall indefinitely in a pending state once Credentials are created but not yet ready; (2) when network policies are enabled, Connectivity pods dial the Ledger gRPC port (8888) but are not covered by any ingress rule in the Ledger v3 NetworkPolicy, causing silent connection drops at runtime; (3) deleting the Connectivity module leaves the privileged Credentials object and distributed secret alive because the object is owned by the Stack with no finalizer or owner reference on the Connectivity CR driving cleanup; (4) if the Credentials CRD or RBAC is absent, the reconciler hard-fails instead of reporting pending; and (5) for stacks using .spec.versionsFromFile, ledger-version resolution reads only from .spec.version, leaving the module stuck with LedgerNotV3 even when the Ledger is running v3. Additionally, ledgerAddress is built from the TLS SNI server name rather than the backend Service endpoint, which breaks on clusters with a non-default DNS domain. Two minor issues also exist: the version-resolution gate fails for versionsFromFile stacks that omit a connectivity key, and a missing backend TLS Secret is surfaced as a hard error rather than a pending condition.

Findings outside the diff

🟠 [major] Connectivity pods blocked by Ledger v3 NetworkPolicy when network policies are enabledinternal/resources/stacks/networkpolicies.go:116

When networkpolicies.enabled is true and Connectivity is enabled, the delegated connectivity workload dials the Ledger directly at ledger-<stack>:8888 (gRPC). The Ledger-v3 ingress NetworkPolicy only allows port 8888 from pods matching the Ledger selector, so Connectivity pods — which are neither gateway nor Ledger pods — have their gRPC connections silently dropped. This manifests as a hard runtime failure on any stack with network policies enabled.

Suggestion: Add an ingress rule to the Ledger v3 NetworkPolicy (or a dedicated NetworkPolicy) that allows pods matching the Connectivity workload pod selector to reach the Ledger gRPC port (8888).

@NumaryBot NumaryBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NumaryBot posted 2 new inline findings.

Summary: #492 (comment)

Comment thread internal/resources/connectivities/init.go Outdated
Comment thread config/crd/bases/formance.com_connectivities.yaml

@NumaryBot NumaryBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NumaryBot review complete: no remaining inline findings.

Resolved 2 stale NumaryBot review threads (1 fixed, 1 outdated).

Summary: #492 (comment)

@Dav-14
Dav-14 force-pushed the feat/connectivity-module branch from f5bf061 to 1f6c39e Compare July 16, 2026 14:40

@NumaryBot NumaryBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NumaryBot posted 1 new inline finding.

Summary: #492 (comment)

Comment thread internal/resources/connectivities/init.go

@NumaryBot NumaryBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NumaryBot posted 1 new inline finding.

Summary: #492 (comment)

Comment thread internal/resources/connectivities/init.go

@NumaryBot NumaryBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NumaryBot review complete: no remaining inline findings.

Resolved 1 stale NumaryBot review thread (1 fixed, 0 outdated).

Summary: #492 (comment)

@NumaryBot NumaryBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NumaryBot posted 1 new inline finding.

Summary: #492 (comment)

Comment thread internal/resources/connectivities/init.go
Base automatically changed from feat/ledger-v3-operator-integration to main July 23, 2026 09:43

@NumaryBot NumaryBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NumaryBot posted 2 new inline findings.

Summary: #492 (comment)

Comment thread internal/resources/connectivities/init.go
Comment thread internal/resources/stacks/networkpolicies.go

@NumaryBot NumaryBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NumaryBot posted 3 new inline findings.

Summary: #492 (comment)

Comment thread internal/resources/connectivities/init.go
Comment thread internal/resources/stacks/networkpolicies.go
Comment thread internal/resources/connectivities/init.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (5)
internal/tests/ledger_v3_controller_test.go (1)

1084-1093: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Register an explicit cleanup for the re-created default configuration.

The DeferCleanup at Line 946 closes over the configuration variable, so reassigning it here is what keeps the second, cluster-scoped DefaultLedgerConfigurationName object from leaking. That's implicit and easy to break (e.g. by shadowing with :=), and a leaked default LedgerConfiguration would affect other specs in this Serial suite.

♻️ Proposed explicit cleanup
 	Expect(Create(configuration)).To(Succeed())
+	DeferCleanup(func() {
+		Expect(client.IgnoreNotFound(Delete(configuration))).To(Succeed())
+	})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tests/ledger_v3_controller_test.go` around lines 1084 - 1093, After
reassigning the cluster-scoped configuration in this test, register an explicit
DeferCleanup for that recreated DefaultLedgerConfigurationName object rather
than relying on the earlier cleanup closure over configuration. Ensure the
cleanup deletes this exact LedgerConfiguration and remains effective even if the
variable is later shadowed.
internal/tests/networkpolicy_controller_test.go (1)

93-98: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert the actual Raft/gRPC port values, not just the count.

allow-ledger-v2-from-v3 below checks the concrete port (8080), but here only HaveLen(2) is checked, so a regression swapping 7777/8888 for other ports would pass.

♻️ Proposed assertion
-					g.Expect(np.Spec.Ingress[0].Ports).To(HaveLen(2))
+					g.Expect(np.Spec.Ingress[0].Ports).To(HaveLen(2))
+					g.Expect(np.Spec.Ingress[0].Ports[0].Port.IntValue()).To(Equal(7777))
+					g.Expect(np.Spec.Ingress[0].Ports[1].Port.IntValue()).To(Equal(8888))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tests/networkpolicy_controller_test.go` around lines 93 - 98, Update
the ingress assertions in the allow-ledger-v2-from-v3 test to verify the
concrete Raft and gRPC port values 7777 and 8888, not only that
np.Spec.Ingress[0].Ports has length two. Preserve the existing count assertion
and assert both expected port entries using the same port-value assertion
pattern as the nearby 8080 check.
deployment/operator/helpers.go (2)

108-121: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Store registry credentials directly instead of re-extracting via an unchecked type assertion.

dc.RegistryAuth[0].(dockerbuild.RegistryArgs) panics if RegistryAuth is ever empty or holds a different concrete type. Since newDockerConfig already has the username/password at construction time, storing them directly on dockerConfig avoids this indexing/type-assertion round-trip.

♻️ Proposed refactor
 type dockerConfig struct {
 	Registry     string
 	PullRegistry string
 	BuilderName  string
 	ImageTag     string
 	Platforms    []string
 	RegistryAuth dockerbuild.RegistryArray
+	Username     pulumi.StringPtrInput
+	Password     pulumi.StringPtrInput
 }
...
+	username := config.GetSecret(ctx, "registry-username")
+	password := config.GetSecret(ctx, "registry-password")
 	return &dockerConfig{
 		...
+		Username: username,
+		Password: password,
 		RegistryAuth: dockerbuild.RegistryArray{
 			dockerbuild.RegistryArgs{
 				Address:  pulumi.String(registry),
-				Username: config.GetSecret(ctx, "registry-username"),
-				Password: config.GetSecret(ctx, "registry-password"),
+				Username: username,
+				Password: password,
 			},
 		},
 	}
...
 		Registry: dockerbuild.RegistryArgs{
 			Address:  pulumi.String(dc.Registry),
-			Username: dc.RegistryAuth[0].(dockerbuild.RegistryArgs).Username,
-			Password: dc.RegistryAuth[0].(dockerbuild.RegistryArgs).Password,
+			Username: dc.Username,
+			Password: dc.Password,
 		},

Also applies to: 185-197

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deployment/operator/helpers.go` around lines 108 - 121, Update dockerConfig
and newDockerConfig to store the registry username and password directly from
config.GetSecret, rather than wrapping them only in RegistryAuth. Replace any
dc.RegistryAuth[0].(dockerbuild.RegistryArgs) indexing and type assertions in
the affected logic with the direct credential fields, preserving the existing
credential values.

94-106: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Arch-to-platform mapping is fragile.

strings.HasSuffix(p, arch) against allPlatforms has no case normalization and offers no explicit way to select multiple platforms (e.g., both amd64+arm64) other than relying on a coincidental shared suffix. Consider accepting a comma-separated list and matching exact platform names.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deployment/operator/helpers.go` around lines 94 - 106, Update the
platform-selection logic around cfg.Get("arch") to normalize architecture values
and parse comma-separated entries, allowing multiple architectures such as amd64
and arm64. Match each requested architecture against explicit platform names in
allPlatforms rather than relying on strings.HasSuffix, while preserving the
linux-<arch> fallback when no valid platforms are selected.
internal/resources/stacks/networkpolicies.go (1)

222-230: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace intstr.FromInt with FromInt32.

intstr.FromInt is deprecated in k8s.io/apimachinery v0.34.2; this helper already converts 64-bit int values for a field backed by an int32, so passing int32 values explicitly avoids the conversion/deprecation issue.

♻️ Proposed refactor
-func networkPolicyTCPPorts(ports ...int) []networkingv1.NetworkPolicyPort {
+func networkPolicyTCPPorts(ports ...int32) []networkingv1.NetworkPolicy Port {
 	protocol := corev1.ProtocolTCP
 	ret := make([]networkingv1.NetworkPolicyPort, 0, len(ports))
 	for _, port := range ports {
-		value := intstr.FromInt(port)
+		value := intstr.FromInt32(port)
 		ret = append(ret, networkingv1.NetworkPolicyPort{Protocol: &protocol, Port: &value})
 		ret = append(ret, networkingv1.NetworkPolicyPort{Protocol: &protocol, Port: &value})
 	}
 	return ret
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/resources/stacks/networkpolicies.go` around lines 222 - 230, Update
networkPolicyTCPPorts to convert each port to int32 and construct the
IntOrString value with FromInt32 instead of the deprecated intstr.FromInt,
preserving the existing NetworkPolicyPort construction and returned results.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@deployment/operator/main.go`:
- Around line 96-98: Update the Helm image value in the operator deployment
configuration to use dc.ImageTag instead of the hardcoded "latest" prefix, while
preserving the existing digest suffix from operatorImage.Digest and repository
value.
- Around line 64-81: Update the licence configuration block to retrieve the
token with the Pulumi secret-aware getter config.GetSecret(ctx, "licence-token")
instead of cfg.Get("licence-token"), and pass that secret output through the
existing licenceValues token entry so the Helm release preserves the token as
masked.

In `@Dockerfile`:
- Around line 28-34: Add a non-root USER directive to the final Dockerfile stage
before the ENTRYPOINT, creating or selecting an unprivileged user that can
execute /usr/bin/operator. Ensure the operator runs under that user by default
while preserving the existing binary copy and entrypoint behavior.

In `@docs/04-Modules/03-Ledger.md`:
- Around line 7-8: Update the PostgreSQL and Broker Markdown links in the module
documentation to use descriptive link text naming each configuration guide
instead of “here,” while preserving the existing destinations and optional
Broker labeling.

In `@docs/09-Configuration` reference/02-Custom Resource Definitions.md:
- Around line 2477-2482: Remove the duplicate ready field from the
GatewayGRPCAPIStatus source schema or generator input, preserving the documented
ready description and the info field, then regenerate the Custom Resource
Definitions reference so the table contains ready only once.
- Around line 2650-2653: Update the `cluster` field description in the
configuration reference so `ClusterSpec` no longer links to the unresolved
`#clusterspec` anchor; link it to the authoritative ClusterSpec reference if
available, otherwise document the type locally or render it as plain text.

In `@internal/resources/connectivities/init_test.go`:
- Around line 199-259: Update the Connectivity controller setup to watch
resources with ledgerCredentialsGVK and map credential events to the affected
Connectivity resource using the credential’s stack selector/namespace data,
since the Credential is not owned by Connectivity. Add an event-driven test
alongside TestEnsureLedgerCredentialsCreatesGodCredentialAndReportsPending and
TestEnsureLedgerCredentialsReportsKeyAndSecretWhenReady that starts with a
pending credential, updates status.phase to Ready, and verifies the
corresponding Connectivity reconciliation is requeued.

In `@internal/resources/gateways/Caddyfile.gotpl`:
- Around line 61-63: Update the GRPCServices branch in the Caddyfile template to
preserve encrypted HTTP/2 by configuring the full protocol set, including h1,
h2, and h2c, instead of only h1 and h2c.

In `@internal/resources/gateways/deployment.go`:
- Around line 124-128: Update the Secret lookup in the gateway deployment
reconciliation loop over sortedSecretNames to detect a not-found error and
return NewPendingError() instead of wrapping it as a hard failure. Preserve the
existing wrapped error for other client.Get failures, and keep the successful
secret-processing path unchanged.

---

Nitpick comments:
In `@deployment/operator/helpers.go`:
- Around line 108-121: Update dockerConfig and newDockerConfig to store the
registry username and password directly from config.GetSecret, rather than
wrapping them only in RegistryAuth. Replace any
dc.RegistryAuth[0].(dockerbuild.RegistryArgs) indexing and type assertions in
the affected logic with the direct credential fields, preserving the existing
credential values.
- Around line 94-106: Update the platform-selection logic around cfg.Get("arch")
to normalize architecture values and parse comma-separated entries, allowing
multiple architectures such as amd64 and arm64. Match each requested
architecture against explicit platform names in allPlatforms rather than relying
on strings.HasSuffix, while preserving the linux-<arch> fallback when no valid
platforms are selected.

In `@internal/resources/stacks/networkpolicies.go`:
- Around line 222-230: Update networkPolicyTCPPorts to convert each port to
int32 and construct the IntOrString value with FromInt32 instead of the
deprecated intstr.FromInt, preserving the existing NetworkPolicyPort
construction and returned results.

In `@internal/tests/ledger_v3_controller_test.go`:
- Around line 1084-1093: After reassigning the cluster-scoped configuration in
this test, register an explicit DeferCleanup for that recreated
DefaultLedgerConfigurationName object rather than relying on the earlier cleanup
closure over configuration. Ensure the cleanup deletes this exact
LedgerConfiguration and remains effective even if the variable is later
shadowed.

In `@internal/tests/networkpolicy_controller_test.go`:
- Around line 93-98: Update the ingress assertions in the
allow-ledger-v2-from-v3 test to verify the concrete Raft and gRPC port values
7777 and 8888, not only that np.Spec.Ingress[0].Ports has length two. Preserve
the existing count assertion and assert both expected port entries using the
same port-value assertion pattern as the nearby 8080 check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 28e6cc05-e158-4ed5-b091-d89ebd421e8e

📥 Commits

Reviewing files that changed from the base of the PR and between 6e288e3 and c641748.

⛔ Files ignored due to path filters (43)
  • config/crd/bases/formance.com_connectivities.yaml is excluded by !**/*.yaml
  • config/crd/bases/formance.com_gatewaygrpcapis.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_ledgerconfigurations.yaml is excluded by !**/*.yaml
  • config/crd/kustomization.yaml is excluded by !**/*.yaml
  • config/rbac/ledgerconfiguration_editor_role.yaml is excluded by !**/*.yaml
  • config/rbac/ledgerconfiguration_viewer_role.yaml is excluded by !**/*.yaml
  • config/rbac/role.yaml is excluded by !**/*.yaml
  • config/samples/formance.com_v1beta1_ledgerconfiguration.yaml is excluded by !**/*.yaml
  • config/samples/kustomization.yaml is excluded by !**/*.yaml
  • deployment/operator/Pulumi.yaml is excluded by !**/*.yaml
  • deployment/operator/go.mod is excluded by !**/*.mod
  • deployment/operator/go.sum is excluded by !**/*.sum, !**/*.sum
  • docs/09-Configuration reference/settings.catalog.json is excluded by !**/*.json
  • go.mod is excluded by !**/*.mod
  • go.sum is excluded by !**/*.sum, !**/*.sum
  • helm/crds/templates/crds/apiextensions.k8s.io_v1_customresourcedefinition_connectivities.formance.com.yaml is excluded by !**/*.yaml
  • helm/crds/templates/crds/apiextensions.k8s.io_v1_customresourcedefinition_gatewaygrpcapis.formance.com.yaml is excluded by !**/*.yaml
  • helm/crds/templates/crds/apiextensions.k8s.io_v1_customresourcedefinition_gatewayhttpapis.formance.com.yaml is excluded by !**/*.yaml
  • helm/crds/templates/crds/apiextensions.k8s.io_v1_customresourcedefinition_gateways.formance.com.yaml is excluded by !**/*.yaml
  • helm/crds/templates/crds/apiextensions.k8s.io_v1_customresourcedefinition_ledgerconfigurations.formance.com.yaml is excluded by !**/*.yaml
  • helm/operator/templates/gen/rbac.authorization.k8s.io_v1_clusterrole_formance-manager-role.yaml is excluded by !**/gen/**, !**/*.yaml, !**/gen/**
  • internal/tests/crds/cert-manager.io_certificates.yaml is excluded by !**/*.yaml
  • internal/tests/crds/cert-manager.io_issuers.yaml is excluded by !**/*.yaml
  • internal/tests/crds/ledger.formance.com_clusters.yaml is excluded by !**/*.yaml
  • internal/tests/testdata/resources/gateway-controller/configmap-with-audit.yaml is excluded by !**/*.yaml
  • internal/tests/testdata/resources/gateway-controller/configmap-with-ledger-and-another-service.yaml is excluded by !**/*.yaml
  • internal/tests/testdata/resources/gateway-controller/configmap-with-ledger-and-grpc.yaml is excluded by !**/*.yaml
  • internal/tests/testdata/resources/gateway-controller/configmap-with-ledger-only.yaml is excluded by !**/*.yaml
  • internal/tests/testdata/resources/gateway-controller/configmap-with-opentelemetry.yaml is excluded by !**/*.yaml
  • tests/e2e/chainsaw/02-stack-lifecycle/asserts/networkpolicies.yaml is excluded by !**/*.yaml
  • tests/e2e/chainsaw/14-ledger-module/chainsaw-test.yaml is excluded by !**/*.yaml
  • tests/e2e/chainsaw/14-ledger-module/resources/database.yaml is excluded by !**/*.yaml
  • tests/e2e/chainsaw/14-ledger-module/resources/stack.yaml is excluded by !**/*.yaml
  • tests/e2e/chainsaw/26-gatewaygrpcapi-sync/chainsaw-test.yaml is excluded by !**/*.yaml
  • tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/gateway.yaml is excluded by !**/*.yaml
  • tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/grpcapi-updated.yaml is excluded by !**/*.yaml
  • tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/grpcapi.yaml is excluded by !**/*.yaml
  • tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/httpapi-ledger.yaml is excluded by !**/*.yaml
  • tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/stack.yaml is excluded by !**/*.yaml
  • tools/kubectl-stacks/go.mod is excluded by !**/*.mod
  • tools/kubectl-stacks/go.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (55)
  • .gitignore
  • Dockerfile
  • Earthfile
  • PROJECT
  • api/formance.com/v1beta1/connectivity_types.go
  • api/formance.com/v1beta1/gateway_types.go
  • api/formance.com/v1beta1/gatewaybackend_types.go
  • api/formance.com/v1beta1/gatewaygrpcapi_types.go
  • api/formance.com/v1beta1/gatewayhttpapi_types.go
  • api/formance.com/v1beta1/ledger_types.go
  • api/formance.com/v1beta1/ledgerconfiguration_types.go
  • api/formance.com/v1beta1/zz_generated.deepcopy.go
  • deployment/operator/.gitignore
  • deployment/operator/helpers.go
  • deployment/operator/main.go
  • docs/04-Modules/03-Ledger.md
  • docs/09-Configuration reference/01-Settings.md
  • docs/09-Configuration reference/02-Custom Resource Definitions.md
  • docs/10-Development/01-Adding a module.md
  • internal/core/setup.go
  • internal/resources/all.go
  • internal/resources/auths/env.go
  • internal/resources/connectivities/init.go
  • internal/resources/connectivities/init_test.go
  • internal/resources/gatewaygrpcapis/create.go
  • internal/resources/gatewaygrpcapis/init.go
  • internal/resources/gatewayhttpapis/create.go
  • internal/resources/gateways/Caddyfile.gotpl
  • internal/resources/gateways/caddyfile.go
  • internal/resources/gateways/caddyfile_test.go
  • internal/resources/gateways/configuration.go
  • internal/resources/gateways/deployment.go
  • internal/resources/gateways/init.go
  • internal/resources/ledgers/exports.go
  • internal/resources/ledgers/init.go
  • internal/resources/ledgers/v3.go
  • internal/resources/ledgers/v3_preview.go
  • internal/resources/ledgers/v3_spec.go
  • internal/resources/ledgers/v3_spec_test.go
  • internal/resources/ledgers/v3_test.go
  • internal/resources/ledgers/v3_tls.go
  • internal/resources/settings/opentelemetry.go
  • internal/resources/stacks/networkpolicies.go
  • internal/tests/application_test.go
  • internal/tests/auth_scopes_settings_test.go
  • internal/tests/gateway_controller_test.go
  • internal/tests/gatewaygrpcapi_controller_test.go
  • internal/tests/jobs_controller_test.go
  • internal/tests/ledger_controller_test.go
  • internal/tests/ledger_v3_controller_test.go
  • internal/tests/networkpolicy_controller_test.go
  • internal/tests/orchestration_controller_test.go
  • internal/tests/registries_test.go
  • internal/tests/transactionplane_controller_test.go
  • internal/tests/wallets_controller_test.go

Comment thread deployment/operator/main.go
Comment on lines +96 to +98
"repository": pulumi.Sprintf("%s/formancehq/operator", dc.PullRegistry),
"tag": pulumi.Sprintf("latest@%s", operatorImage.Digest),
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Helm image tag hardcodes "latest" while the actual pushed tag is dc.ImageTag.

buildImage pushes the index as %s/%s:%s using dc.ImageTag (commit/timestamp-based), never as latest. The Helm value here uses the literal "latest@%s" instead, so the human-readable tag component doesn't correspond to any tag actually pushed to the registry — confusing even though the digest pin still resolves correctly on pull.

🐛 Proposed fix
 				"image": pulumi.Map{
 					"repository": pulumi.Sprintf("%s/formancehq/operator", dc.PullRegistry),
-					"tag":        pulumi.Sprintf("latest@%s", operatorImage.Digest),
+					"tag":        pulumi.Sprintf("%s@%s", dc.ImageTag, operatorImage.Digest),
 				},
📝 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
"repository": pulumi.Sprintf("%s/formancehq/operator", dc.PullRegistry),
"tag": pulumi.Sprintf("latest@%s", operatorImage.Digest),
},
"repository": pulumi.Sprintf("%s/formancehq/operator", dc.PullRegistry),
"tag": pulumi.Sprintf("%s@%s", dc.ImageTag, operatorImage.Digest),
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deployment/operator/main.go` around lines 96 - 98, Update the Helm image
value in the operator deployment configuration to use dc.ImageTag instead of the
hardcoded "latest" prefix, while preserving the existing digest suffix from
operatorImage.Digest and repository value.

Comment thread Dockerfile
Comment thread docs/04-Modules/03-Ledger.md Outdated
Comment on lines +2477 to +2482
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `ready` _boolean_ | Ready indicates if the resource is seen as completely reconciled | | |
| `info` _string_ | Info can contain any additional like reconciliation errors | | |
| `ready` _boolean_ | | | |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the duplicate ready status field.

GatewayGRPCAPIStatus documents ready twice, with the second entry lacking a description. Fix the source schema/doc generator and regenerate this reference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/09-Configuration` reference/02-Custom Resource Definitions.md around
lines 2477 - 2482, Remove the duplicate ready field from the
GatewayGRPCAPIStatus source schema or generator input, preserving the documented
ready description and the info field, then regenerate the Custom Resource
Definitions reference so the table contains ready only once.

Comment thread docs/09-Configuration reference/02-Custom Resource Definitions.md Outdated
Comment thread internal/resources/connectivities/init_test.go
Comment on lines +61 to +63
{{- if .GRPCServices }}
protocols h1 h2c
{{- end }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Caddy servers protocols directive default h1 h2 h2c

💡 Result:

In Caddy, the protocols directive specifies which HTTP protocols are enabled for a server [1][2]. The default protocols are ["h1", "h2", "h3"] [1][3][4]. Supported values are: - h1: HTTP/1.1 - h2: HTTP/2 (requires TLS) - h2c: HTTP/2 cleartext (no TLS) - h3: HTTP/3 (requires QUIC/UDP) Key implementation details include: - When h2 or h2c is enabled, h1 must also be included because of Go standard library limitations [1][5][2]. - HTTP/2 operates over TLS, while HTTP/3 uses a separate UDP socket [1][5][3]. - H2C is not implemented by the Go standard library and is generally recommended against unless necessary, such as for specific gRPC proxying scenarios [1][2]. - If you leave the protocols directive empty or unset, Caddy defaults to ["h1", "h2", "h3"] [1][3][4]. The directive can be configured globally within a servers block in the Caddyfile [2]: servers:443 { protocols h1 h2 h3 } Or in JSON configuration: { "apps": { "http": { "servers": { "example": { "protocols": ["h1", "h2", "h3"] } } } } }

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
fd -a 'Caddyfile\.gotpl|internal/resources/gateways|resources/gateways' . | sed 's#^\./##' | head -100

echo
echo "Target file excerpt:"
if [ -f internal/resources/gateways/Caddyfile.gotpl ]; then
  sed -n '1,240p' internal/resources/gateways/Caddyfile.gotpl | nl -ba | sed -n '1,240p'
else
  echo "internal/resources/gateways/Caddyfile.gotpl not found"
fi

echo
echo "Protocol/server listener context searches:"
rg -n "ports|:{{ \.\Port }}|listen|tls|GRPCServices|transport http|protocols h1 h2c|protocols h1 h2 h2c" internal/resources/gateways -S || true

echo
echo "Template usages / Port value:"
rg -n "GRPCServices|Port|grpc|Gateway" --glob '*.go' --glob '*.ts' --glob '*.ts' internal -S | head -200

Repository: formancehq/operator

Length of output: 353


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Target file excerpt:"
sed -n '1,240p' internal/resources/gateways/Caddyfile.gotpl

echo
echo "Protocol/server listener context searches:"
rg -n "ports|:{{ \.\Port }}|listen|tls|GRPCServices|transport http|protocols h1 h2c|protocols h1 h2 h2c" internal/resources/gateways -S || true

echo
echo "Template usages / Port value:"
rg -n "GRPCServices|Port|grpc|Gateway" --glob '*.go' --glob '*.ts' internal -S | head -200

Repository: formancehq/operator

Length of output: 26996


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Gateway listener/deployment/service/ingress creation:"
sed -n '1,210p' internal/resources/gateways/deployment.go
echo
sed -n '1,170p' internal/resources/gateways/ingress.go
echo
sed -n '1,210p' internal/resources/gateways/configuration.go
echo
rg -n -F "ServicePort" internal/resources/gateways -S

Repository: formancehq/operator

Length of output: 11290


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Service creation:"
rg -n "createService|NewService|ServicePort|ServiceName|name: .*gateway|port: .*8080|name: http|protocol: TLS|tls" internal/resources/gateways internal/resources/services internal/resources/applications -S

echo
fd -a Caddyfile\* . | sed 's#^\./##'
echo
sed -n '1,120p' internal/resources/payments/Caddyfile.gotpl

Repository: formancehq/operator

Length of output: 3042


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Gateway service creation:"
rg -n -C 8 "CreateOrUpdate\\[\\*corev1\\.Service\\]|\"http\"|Port:" internal/resources/gateways/gateways.go internal/resources/gateways -X '*.go' -S || true

echo
echo "Full services helper:"
sed -n '1,90p' internal/resources/services/services.go

echo
echo "Full Gateway reconcile/service code:"
sed -n '102,210p' internal/resources/gateways/gateways.go

Repository: formancehq/operator

Length of output: 2507


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Gateway reconcile/service create call:"
rg -n -C 12 'Create\(|WithDefault|core\.CreateOrUpdate\[\*corev1\.Service\]' internal/resources/gateways/gateways.go internal/resources/gateways -S

echo
echo "Read-only structural/data verifier:"
python3 - <<'PY'
from pathlib import Path
for p in ["internal/resources/gateways/Caddyfile.gotpl", "internal/resources/gateways/ingress.go", "internal/resources/services/services.go"]:
    text = Path(p).read_text()
    print(f"\n== {p} ==")
    print("contains `protocols h1 h2c`:", "protocols h1 h2c" in text)
    print("contains listen/targeting `:{{ .Port }}`:", ":{{ .Port }}" in text)
    print("contains Ingress TLS secret name:", "tlsEnabled" in text and "withTls" in text)
    print("contains ingress backend service port name \"http\":", 'Port: v1.ServiceBackendPort{\n\t\t\t\t\t\tName: "http",' in text)
    print("creates service port named http with Port 8080:", 'Name:       cfg.PortName,\n\t\t\t\t\tPort:       cfg.Port,' in text and 'PortName:    "http",' in text and 'Port:        8080,' in text)
    print("contains h2 enablement:", "h2" in text)
PY

Repository: formancehq/operator

Length of output: 2865


🌐 Web query:

Caddy servers option protocols directive default h1 h2 h3 h2c no explicit setting

💡 Result:

In the Caddy web server, the default setting for the protocols directive is [h1 h2 h3] [1][2][3][4]. This means that by default, Caddy enables HTTP/1.1 (h1), HTTP/2 (h2), and HTTP/3 (h3) [1][3]. When no explicit protocols directive is configured, Caddy automatically applies these defaults to all servers [1][3]. Important technical considerations regarding these protocols include: - H2 (HTTP/2) and H2C (HTTP/2 Cleartext) Requirements: Due to limitations in the Go standard library, if you explicitly enable h2 or h2c, you must also enable h1 [1][2][4]. - Transport Mechanisms: HTTP/2 operates exclusively over TLS (HTTPS), while HTTP/3 operates over UDP using the QUIC protocol [1][2]. - H2C (Cleartext HTTP/2): H2C is not enabled by default and is generally discouraged unless specifically required, such as for certain gRPC proxying scenarios where encryption is not feasible [1][4]. H2C operates over plain TCP [1][3]. - Configuration Scope: The protocols directive can be configured globally within the Caddyfile servers block or applied to specific server blocks by matching the listener address [5][4][6]. Caddy recommends that most users rely on the default settings to ensure optimal performance and compatibility [1].

Citations:


Preserve the default HTTPS/HTTP protocol set.

The gateway listener is used behind ingress TLS, so protocols h1 h2c disables h2 for encrypted clients. Use the full protocol set instead of h1 h2c alone.

🔧 Proposed fix
 		{{- if .GRPCServices }}
-		protocols h1 h2c
+		protocols h1 h2 h2c h3
 		{{- end }}
📝 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
{{- if .GRPCServices }}
protocols h1 h2c
{{- end }}
{{- if .GRPCServices }}
protocols h1 h2 h2c h3
{{- end }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/resources/gateways/Caddyfile.gotpl` around lines 61 - 63, Update the
GRPCServices branch in the Caddyfile template to preserve encrypted HTTP/2 by
configuring the full protocol set, including h1, h2, and h2c, instead of only h1
and h2c.

Comment thread internal/resources/gateways/deployment.go
David Ragot and others added 9 commits July 27, 2026 16:19
Introduce a new `formance.com/v1beta1 Connectivity` stack module, mirroring
the Ledger v3 delegation pattern.

The module does not run the workload itself. It:

- detects, at controller start-up, whether the connectivity operator
  (`connectivity.formance.com` Connectivity CRD) is installed and reachable
  with the required RBAC — the same capability + API-group probe the Ledger
  v3 module uses (CRD served-version check + SelfSubjectAccessReview per
  verb). When absent, the module reports the capability as unavailable and
  stays pending instead of failing the controller.
- gates on the stack's ledger being v3 and ready (connectivity ingests into
  the Ledger v3 gRPC endpoint).
- provisions a `connectivity.formance.com/v1alpha1 Connectivity` resource
  bound to that ledger: `ledgerAddress` = the ledger v3 gRPC service and
  `ledgerTLS` = the ledger backend TLS secret. The connection details are
  taken from `ledgers.V3GRPCBackendRef`, the single source of truth already
  used to reach the ledger over gRPC, so connectivity and the gateway stay in
  sync.
- reflects the delegated resource's readiness back onto the module status.

Includes the module type, reconciler + capability detection, unit tests for
the capability-gating paths, and the generated CRD/RBAC/deepcopy + helm CRD.
Address review feedback and the Dirty check:

- Resolve the ledger version with core.ResolveModuleVersion so the v3 gate
  also works for stacks using spec.versionsFromFile (previously the version
  fell back to empty and Connectivity stayed stuck on LedgerNotV3).
- Register bases/formance.com_connectivities.yaml in config/crd/kustomization
  so non-Helm (kustomize) installs create the Connectivity CRD.
- Regenerate CRD reference docs + helm CRD (just pre-commit).
Document the current end-to-end process for adding a stack module: the
module CR type (incl. the mandatory formance.com/kind=module label), the
reconciler + init registration, all.go + config/crd/kustomization
registration, capability detection for delegating modules, version
resolution, codegen, and the deployment gotchas (reconcileStrategy: Revision,
the startup-only capability probe, and versionsFromFile requirements).
The delegated connectivity.formance.com Connectivity was created without
spec.image, so the connectivity operator fell back to its built-in
ghcr.io/formancehq/connectivity-core:latest default — bypassing the stack's
registry rewrite (e.g. ghcr.io -> registry.v2.formance.dev) and pull secrets,
which makes it unpullable on rewritten registries.

Resolve the connectivity-core image via registries.GetFormanceImage (using the
Connectivity module version) so it honours the stack registry settings, and set
spec.image + spec.imagePullSecrets on the delegated resource.
- Always enable the connectivity-api companion on the delegated Connectivity
  (spec.api.enabled=true), resolving the connectivity-api image through the
  registry translation so it honours the stack's registry rewrite + pull
  secrets (not the connectivity operator's ghcr.io/...:latest default).
- Register a GatewayHTTPAPI for the module routing /api/connectivity to the
  connectivity-api Service (<stack>-api:8080) the connectivity operator
  provisions, and own it so changes reconcile.
…e auth

The connectivity module deployed the delegated Connectivity CR but never wired
connectivity-core's authentication to the stack's Ledger v3 gRPC endpoint, so
connectivity-core sent no token and the ledger rejected every call with
'requires scope ledger:LedgerWrite'.

Provision a god-mode ledger.formance.com/Credentials (cluster-scoped, owned by
the Stack) selecting the stack's ledger Cluster: the ledger operator generates
the Ed25519 keypair, registers the public key on the ledger, and distributes
the private seed as a Secret in the stack namespace. Once Ready, wire the
Connectivity CR's spec.auth (keyId + secretKeyRef->seed.hex) so the connectivity
operator passes --auth-key-id/--auth-key-file and connectivity-core signs its
gRPC tokens with the registered key. Regenerates RBAC for the new resource.
The connectivity repo's CI publishes formancehq/connectivity; the
former connectivity-core repository no longer exists, so the module
kept writing an unpullable image on the delegated resource.
The kubebuilder markers for ledger.formance.com credentials (added with
the connectivity module) were present in config/rbac but the helm chart's
generated ClusterRole was never refreshed, so the deployed operator was
forbidden from listing Credentials -- its informer never synced and every
Connectivity reconcile hung before reaching the delegated resource.
Main's ledgerV3GRPCBackendRef now takes the ledger Cluster's configured
gRPC port; keep the connectivity-facing export on the default port.
@Dav-14
Dav-14 force-pushed the feat/connectivity-module branch from 3795ef8 to a6746a0 Compare July 27, 2026 14:24

@NumaryBot NumaryBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NumaryBot posted 1 new inline finding.

Summary: #492 (comment)

Comment thread internal/resources/ledgers/exports.go Outdated
…ed resource (#500)

* feat(connectivity): provision OpenTelemetry monitoring on the delegated resource

Resolve the stack's OpenTelemetry configuration with
settings.GetOpenTelemetryConfiguration (collector-aware: points at
otel-collector.<stack>:4318 when the per-stack collector exists, else honours
the opentelemetry.* Settings) and embed it inline in the delegated
Connectivity's spec.monitoring, mirroring the Ledger v3 Cluster pattern. The
connectivity operator turns spec.monitoring into OTEL_* env vars on the
workload; there is no separate Monitoring object to reference.

The whole spec.monitoring block is rebuilt on every reconcile and pruned when
telemetry is disabled, so the reconcile stays idempotent.

Not-tested: end-to-end env-var emission by the connectivity operator (covered by
the connectivity repo); unit tests assert the inline spec mapping and idempotency.

* fix(connectivity): drop unresolvable pod-name attribute from delegated monitoring

GetOpenTelemetryConfiguration injects pod-name=$(POD_NAME), which only
resolves when a downward-API POD_NAME env var is defined ahead of
OTEL_RESOURCE_ATTRIBUTES. The connectivity operator emits
OTEL_RESOURCE_ATTRIBUTES verbatim from spec.monitoring.attributes and
defines no such env var, so the placeholder surfaced literally in the
delegated workload's telemetry.

Strip attributes whose value carries an unresolvable $(...) placeholder
before forwarding, keeping literal resource attributes (stack, custom).
Omit the attributes field entirely when nothing resolvable remains.

@NumaryBot NumaryBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NumaryBot posted 4 new inline findings.

Summary: #492 (comment)

Comment thread internal/resources/connectivities/init.go
Comment thread internal/resources/connectivities/init.go
Comment thread internal/resources/connectivities/init.go
Comment thread internal/resources/connectivities/init.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
docs/10-Development/01-Adding a module.md (1)

43-49: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Document watches for every readiness dependency.

Line 48 lists only Ledger. The Connectivity reconciler also depends on the ledger Credentials resource. State that delegated modules must watch every readiness-gating resource. Otherwise, the module can remain pending after Credentials becomes ready because no event triggers another reconcile.

Proposed documentation update
             WithWatchDependency[*v1beta1.<Module>](&v1beta1.Ledger{}), // re-reconcile on dependency change
         ),
     )
 }
+
+For delegated modules, add a watch for every readiness-gating resource,
+including the ledger `Credentials` resource when credential readiness controls
+provisioning.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/10-Development/01-Adding` a module.md around lines 43 - 49, Update the
module setup documentation around WithWatchDependency to state that delegated
modules must watch every readiness-gating resource, including both the Ledger
and its Credentials resource. Clarify that each dependency required by the
reconciler’s readiness checks needs a watch so readiness changes trigger
reconciliation.
internal/resources/connectivities/init_test.go (1)

407-428: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Add a Reconcile happy-path test.

The only Reconcile test covers the capability-unavailable pending path. No test exercises the full success path: a ready Ledger v3, ready credentials, successful image resolution, and the resulting spec fields (ledgerAddress, ledgerTLS, auth.keyId, auth.secretKeyRef) on the created delegated Connectivity resource. Given this function is the central reconciliation path for the module, add a test with a fake client seeded with a ready Ledger, a ready ledger Credentials object, and the required scheme registrations, then assert on the resulting unstructured object's spec.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/resources/connectivities/init_test.go` around lines 407 - 428, Add a
happy-path test alongside
TestConnectivityReconcilePendingWhenCapabilityUnavailable that seeds a fake
client with a ready Ledger v3 and ready ledger Credentials, registers the
required schemes, and configures successful image resolution before calling
Reconcile. Assert that the created delegated Connectivity resource contains the
expected spec.ledgerAddress, spec.ledgerTLS, spec.auth.keyId, and
spec.auth.secretKeyRef values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/resources/connectivities/init.go`:
- Around line 500-509: In internal/resources/connectivities/init.go at lines
500-509, register a watch handler that maps status updates of the ledger
Credentials object (ledgerCredentialsGVK, named connectivity-<stack>) back to
the corresponding Connectivity reconcile request. This watch should be added to
the Init call, either directly via a Watches handler or integrated into the
withConnectivityClusterWatch options pattern, so that when the Credentials
object status changes, the Connectivity resource is requeued. In
internal/resources/connectivities/init_test.go at lines 201-261, add a test that
verifies this requeue path by starting with a pending Credentials object,
updating its status.phase to Ready, and asserting that the corresponding
Connectivity reconcile is requeued as a result of the watch firing.
- Around line 163-167: Update the Connectivity initialization around
V3GRPCBackendRef so it uses the stack’s configured Ledger v3 gRPC port instead
of the default-port helper behavior. Pass or propagate the relevant configured
port into the backend reference construction, ensuring ledgerAddress is built
from the same non-default port used by Gateway.
- Around line 345-395: Add a finalizer-based cleanup mechanism to the
Connectivity resource that ensures the associated Credentials object is deleted
during Connectivity deletion. In the reconciliation logic, add a finalizer
constant for Connectivity deletion and check if the Connectivity resource is
being deleted; when deletion is detected, delete the cluster-scoped Credentials
object named connectivity-<stack.Name> (using the same naming pattern as in
ensureLedgerCredentials) before removing the finalizer from the Connectivity
resource. This ties the privileged Credentials and its distributed Secret to the
Connectivity module lifecycle so they are revoked when the module is removed,
regardless of the Stack's lifecycle.

---

Nitpick comments:
In `@docs/10-Development/01-Adding` a module.md:
- Around line 43-49: Update the module setup documentation around
WithWatchDependency to state that delegated modules must watch every
readiness-gating resource, including both the Ledger and its Credentials
resource. Clarify that each dependency required by the reconciler’s readiness
checks needs a watch so readiness changes trigger reconciliation.

In `@internal/resources/connectivities/init_test.go`:
- Around line 407-428: Add a happy-path test alongside
TestConnectivityReconcilePendingWhenCapabilityUnavailable that seeds a fake
client with a ready Ledger v3 and ready ledger Credentials, registers the
required schemes, and configures successful image resolution before calling
Reconcile. Assert that the created delegated Connectivity resource contains the
expected spec.ledgerAddress, spec.ledgerTLS, spec.auth.keyId, and
spec.auth.secretKeyRef values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 62aaed12-7a50-4b92-bcf3-128097420cf8

📥 Commits

Reviewing files that changed from the base of the PR and between c641748 and bd8285c.

⛔ Files ignored due to path filters (5)
  • config/crd/bases/formance.com_connectivities.yaml is excluded by !**/*.yaml
  • config/crd/kustomization.yaml is excluded by !**/*.yaml
  • config/rbac/role.yaml is excluded by !**/*.yaml
  • helm/crds/templates/crds/apiextensions.k8s.io_v1_customresourcedefinition_connectivities.formance.com.yaml is excluded by !**/*.yaml
  • helm/operator/templates/gen/rbac.authorization.k8s.io_v1_clusterrole_formance-manager-role.yaml is excluded by !**/gen/**, !**/*.yaml, !**/gen/**
📒 Files selected for processing (8)
  • api/formance.com/v1beta1/connectivity_types.go
  • api/formance.com/v1beta1/zz_generated.deepcopy.go
  • docs/09-Configuration reference/02-Custom Resource Definitions.md
  • docs/10-Development/01-Adding a module.md
  • internal/resources/all.go
  • internal/resources/connectivities/init.go
  • internal/resources/connectivities/init_test.go
  • internal/resources/ledgers/exports.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/resources/all.go
  • internal/resources/ledgers/exports.go
  • api/formance.com/v1beta1/connectivity_types.go

Comment thread internal/resources/connectivities/init.go
Comment thread internal/resources/connectivities/init.go
Comment thread internal/resources/connectivities/init.go

@flemzord flemzord left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed as the root of the linked stack. The current head still contains the credential watch/cleanup, closed-gate teardown, configurable gRPC port, gateway TLS pending-state, and NetworkPolicy issues tracked in the existing threads; those fixes live in descendant PRs #501#507 rather than in this head. I am not duplicating the inline findings or approving this root in isolation. It is safe to reconsider once the stack is merged/restacked so those fixes are part of the effective change.

Dav-14 added 7 commits August 6, 2026 10:59
When a backend module has not yet provisioned its TLS Secret, the Gateway
deployment reconciler fetched the Secret and returned any error as a hard
failure. A NotFound during this race surfaced the Gateway as errored rather
than pending.

Return core.NewPendingError() on apierrors.IsNotFound so the framework treats
it as pending (matching how other 'not ready yet' conditions are handled) and
retries. All other Get errors remain hard errors.
…etriggered (#504)

The Connectivity reconciler returns a PendingError while the cluster-scoped
ledger Credentials it provisions (connectivity-<stack>) is not yet Ready
(LedgerCredentialsPending). The reconcile loop treats a PendingError as a
terminal ctrl.Result{} with no RequeueAfter, and the Credentials is owned by
the Stack rather than the namespaced Connectivity, so nothing re-triggered the
module when the ledger operator flipped the Credentials status.phase to Ready:
the reconcile could stall indefinitely.

Register a raw builder watch on the ledger.formance.com/v1alpha1 Credentials
GVK (unstructured, mirroring withConnectivityClusterWatch) that maps a
Credentials event back to the Connectivity in the matching stack, derived from
the connectivity-<stack> name and listed via the stack field index. The watch
is gated on the Credentials CRD being installed so controller setup never fails
when the ledger operator is absent. RBAC already grants watch on credentials.

Add unit tests for the mapping (enqueues the matching Connectivity, returns
nothing for a stack without one, ignores foreign Credentials) and for the
capability gate (disabled when the CRD is absent or discovery fails).
… closes (#505)

* fix(connectivity): tear down delegated resources when the ledger gate closes

When a stack had already provisioned the delegated Connectivity and its
GatewayHTTPAPI, closing a hard Ledger gate on a later reconcile only set a
pending condition and returned; the delegated workload kept running and stayed
exposed through the gateway even though its prerequisite no longer held.

Introduce teardownDelegated(ctx, stack, connectivity), which idempotently
deletes both the delegated Connectivity and the GatewayHTTPAPI
(client.IgnoreNotFound). Call it from the hard/persistent gates only:

- LedgerNotFound  (module removed)      -> teardown
- LedgerNotV3     (real downgrade)      -> teardown

Leave the transient gates untouched so a momentary blip does not flap the
workload:

- LedgerVersionUnresolved (resolution error, not a downgrade) -> keep
- LedgerNotReady          (v3 but momentarily not ready)      -> keep

Add unit tests covering both the teardown-on-not-v3 and keep-on-v3-not-ready
paths.

* fix(connectivity): tear down credentials and route independently on hard gate close

teardownDelegated returned on the first delete error, so a failed
delegated-Connectivity deletion left the public GatewayHTTPAPI route
exposed; it also never deleted the cluster-scoped god-mode Credentials,
whose distributed private-key Secret stack-namespace GC never reclaims
(and whose public key stays registered on the v3 Cluster).

Attempt all three deletions independently via errors.Join and delete the
Credentials (which cascades the ledger operator's key deregistration and
Secret cleanup). Tests: assert Credentials cleanup on hard teardown, its
retention on a transient gate, and that every deletion is attempted when
one fails (raised in review of #505).

* fix(connectivity): tear down on a closed ledger gate even without the operator

The !connectivityAvailable guard returned before the LedgerNotFound/
LedgerNotV3 teardown, so if the connectivity operator became unavailable
after resources were provisioned and the ledger was then removed or
downgraded, the gateway route and god-mode Credentials stayed behind.

Evaluate the ledger hard gate (ledgerGateClosed) in the capability-
unavailable branch and tear down when it is closed, guarded so a transient
operator outage with a healthy v3 ledger does not flap the resources.
teardownDelegated now tolerates the connectivity CRD being absent
(ignoreAbsent) so the delete is a no-op when the API is gone. Tests cover
teardown-on-closed-gate and retention-on-open-gate (raised in review of #505).
V3GRPCBackendRef, the single source of truth consumed by the
connectivity module to build its ledgerAddress, always passed port 0 to
ledgerV3GRPCBackendRef and therefore assumed the default gRPC port. The
gateway backend (v3.go / v3_preview.go) instead resolves the port from
the stack LedgerConfiguration (spec.cluster.service.grpcPort), so a stack
overriding the ledger Cluster gRPC service port got a Connectivity
pointed at the wrong port while the gateway stayed correct.

Resolve the configured port from the LedgerConfiguration inside
V3GRPCBackendRef (via ledgerV3BaseSpec, the same base the gateway derives
its clusterSpec from) so both consumers honour the override and fall back
to the default port when unset. Thread the reconciler Context through the
export and its connectivity caller.

Add a table-driven unit test covering default, stack-scoped, wildcard,
and precedence cases.
…a NetworkPolicy (#507)

* fix(connectivity): allow connectivity pods to reach Ledger v3 gRPC via NetworkPolicy

When networkpolicies.enabled, the default-deny-ingress policy drops all
ingress to Ledger v3 pods except the explicitly-allowed gateway and
intra-cluster ledger peers. The delegated connectivity workload dials the
Ledger v3 gRPC endpoint (ledgers.V3GRPCBackendRef, port 8888) directly, but
connectivity pods are neither gateway nor ledger pods, so their gRPC
connections were silently dropped on network-policy stacks.

Add a dedicated allow-ledger-v3-from-connectivity NetworkPolicy granting the
connectivity workload ingress to the Ledger v3 pods on the gRPC port (8888),
following the existing dedicated-policy pattern (allow-from-gateway).

The connectivity pod labels are owned by the connectivity operator (separate
repo) and cannot be confirmed here; connectivitySelector matches the
operator-wide convention app.kubernetes.io/name=connectivity (documented as an
assumption in code). Add unit tests rendering the policy and extend the
network-policy controller test.

* fix(stacks): leave connectivity->ledger-v3 gRPC port unrestricted

The allow-ledger-v3-from-connectivity policy hardcoded port 8888, so a
stack overriding spec.cluster.service.grpcPort had its connectivity gRPC
traffic silently denied by the default-ingress policy.

Rather than resolving the port (which would go stale unless the Stack
controller also watched LedgerConfiguration), leave the port unrestricted
for this tightly scoped same-namespace connectivity->ledger-v3 pair,
mirroring allow-ledger-v3-cluster which is already port-agnostic for
exactly this reason. The rule can no longer be broken or left stale by a
grpcPort override, and needs no config watch.
)

The delegated connectivity.formance.com/Connectivity is namespaced (one
per stack namespace), so it no longer needs a stack-scoped name. Give it
the fixed name "connectivity"; the connectivity operator derives the API
Service from it, so it becomes "connectivity-api" instead of "<stack>-api".

Credentials (connectivity-<stack>) and the GatewayHTTPAPI
(<stack>-connectivity) stay stack-scoped — they are cluster-scoped and need
the stack for uniqueness. No migration: the module is not yet released.

@NumaryBot NumaryBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NumaryBot posted 7 new inline findings.

Summary: #492 (comment)

setCondition(connectivity, metav1.ConditionFalse, "LedgerCredentialsFailed", err.Error())
return err
}
if !credReady {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [major] No watch on ledger Credentials — reconciler can stall indefinitely
reported by NumaryBot, coderabbitai[bot]

When the Connectivity reconciler creates the ledger Credentials object and returns a pending/application error waiting for status.phase=Ready, it will not be requeued automatically because the Credentials resource is not registered as a watched object. The reconciler only watches GatewayHTTPAPI, the delegated Connectivity, settings, and Ledger. On a normal install the controller can remain stuck in LedgerCredentialsPending until some unrelated watched object triggers a reconcile.

Suggestion: Add a Watch for the ledger Credentials resource in the controller setup so that a status update on that object (e.g. phase→Ready) triggers a reconcile of the owning Connectivity. Alternatively, use a periodic requeue (e.g. ctrl.Result{RequeueAfter: someInterval}) whenever the reconciler returns while waiting for Credentials readiness.

cred.SetGroupVersionKind(ledgerCredentialsGVK)
cred.SetName("connectivity-" + stack.Name)
if _, err = controllerutil.CreateOrUpdate(ctx, ctx.GetClient(), cred, func() error {
if err := controllerutil.SetControllerReference(stack, cred, ctx.GetScheme()); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [major] Ledger credentials not cleaned up when Connectivity module is deleted

When a Connectivity module is removed but the Stack remains, the cluster-scoped Credentials object (connectivity-<stack>) is kept alive because it is owned by the Stack rather than the Connectivity module. There is no Connectivity finalizer or cleanup path that deletes this object, so the privileged ledger credential and distributed secret remain registered even after the module is disabled or deleted, leaving access that was provisioned but never revoked.

Suggestion: Add a finalizer to the Connectivity resource that explicitly deletes the associated Credentials object (and any distributed secret) during deletion. Alternatively, set the Credentials object's owner reference to the Connectivity CR so Kubernetes garbage collection handles cleanup automatically.

// authenticate its gRPC calls. The ledger operator registers the public key
// on the ledger and distributes the private seed as a Secret in the stack
// namespace; connectivity-core is wired to it via spec.auth below.
authKeyID, authSecretName, credReady, err := ensureLedgerCredentials(ctx, stack)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [major] Missing ledger Credentials CRD/RBAC causes hard failure instead of pending

When the ledger.formance.com Credentials CRD or its RBAC is absent (e.g. an older ledger-operator install), ensureLedgerCredentials returns a hard NoMatch/Forbidden error and the module is marked failed. Since Credentials is an external capability needed only for Connectivity, this scenario should be treated as a pending/unavailable condition rather than a reconciliation failure.

Suggestion: Detect NoMatch and Forbidden errors from the Credentials API call and convert them into a NewPendingError() (or an explicit unavailable condition) so the module reports pending rather than failed when the CRD is not yet installed.

setCondition(connectivity, metav1.ConditionFalse, "LedgerBackendResolveFailed", err.Error())
return err
}
ledgerAddress := fmt.Sprintf("%s:%d", backend.TLS.ServerName, backend.Port)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [major] ledgerAddress uses SNI server name instead of the backend Service endpoint

ledgerAddress is built from backend.TLS.ServerName, which is the SNI/certificate hostname. In clusters whose DNS domain is not cluster.local, ledger-<stack>.<stack>.svc.cluster.local may not resolve. The gateway upstream path uses backend.Name as the Service address and TLS.ServerName only for TLS verification; ledgerAddress should follow the same pattern.

Suggestion: Build ledgerAddress from the backend Service name/endpoint rather than backend.TLS.ServerName, and use TLS.ServerName only for the TLS SNI/verification field.

// connectivity-core can authenticate its gRPC calls to the stack's Ledger v3:
// the ledger operator generates the Ed25519 keypair, registers the public key
// on the matched ledger Cluster, and distributes the private seed as a Secret.
ledgerCredentialsGVK = schema.GroupVersionKind{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [major] Ledger version resolved only from spec.version, not from versionsFromFile

For stacks that use .spec.versionsFromFile instead of .spec.version, the fallback never reads the Ledger entry from the referenced Versions object, so ledgerVersion stays empty and Connectivity remains stuck with LedgerNotV3 even when the Ledger module itself reconciled as v3 and is ready.

Suggestion: Use the same module-version resolution path as the Ledger reconciler (reading from the referenced Versions object when versionsFromFile is set) before applying the v3 gate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

C'est pas mon probleme


func init() {
Init(
WithModuleReconciler(Reconcile,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 [minor] Version resolution fails on versionsFromFile stacks without a 'connectivity' entry

For stacks that use spec.versionsFromFile, the module framework resolves the Connectivity version before entering Reconcile. If the referenced Versions object has no 'connectivity' key, reconciliation fails before reaching the capability/Ledger-v3 gate, even though the resolved version value is never actually used by the reconciler. This forces operators to add a meaningless version entry to their Versions file just to unblock reconciliation.

Suggestion: Make the version field optional or provide a sensible default (e.g. empty string) so the reconciler can proceed when no connectivity version is specified. If the version is genuinely unused, skip the version-resolution gate entirely for this module.

return core.NewPendingError().WithMessage("waiting for backend TLS Secret %s/%s", namespace, secretName)
}
return fmt.Errorf("getting backend TLS Secret %s/%s: %w", namespace, secretName, err)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 [minor] Missing backend TLS Secret treated as hard error instead of pending

If the referenced backend TLS Secret doesn't exist yet (e.g., race with the owning module provisioning it), the code returns a hard error instead of a pending state — inconsistent with the 'broker not ready' handling in internal/resources/gateways/init.go which uses NewPendingError(). The labeled-secret watch will eventually retrigger reconciliation, but until then the Gateway will surface as errored rather than pending.

Suggestion: Check for apierrors.IsNotFound(err) and return core.NewPendingError().WithMessage(...) instead of a hard error when the Secret is not yet available.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/resources/connectivities/init.go`:
- Around line 316-321: Update the doc comment for teardownDelegated to state
that the delegated Connectivity uses connectivityDelegatedName ("connectivity")
as its name and the stack name as its namespace; retain the existing
GatewayHTTPAPI naming and scoping description.
- Around line 85-91: Use ledgerCredentialsWatchAvailable in Reconcile when
handling LedgerCredentialsPending: if the watch is unavailable, return a delayed
requeue result so pending Connectivity resources retry without CRD events;
preserve the existing NewPendingError behavior and watch-driven path when the
flag is true.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c0075e17-fb3c-46d9-b7b5-9e9bf747f064

📥 Commits

Reviewing files that changed from the base of the PR and between bd8285c and bf154c2.

📒 Files selected for processing (9)
  • internal/resources/connectivities/init.go
  • internal/resources/connectivities/init_test.go
  • internal/resources/gateways/deployment.go
  • internal/resources/gateways/deployment_test.go
  • internal/resources/ledgers/exports.go
  • internal/resources/ledgers/exports_test.go
  • internal/resources/stacks/networkpolicies.go
  • internal/resources/stacks/networkpolicies_test.go
  • internal/tests/networkpolicy_controller_test.go

Comment on lines +85 to +91

// ledgerCredentialsWatchAvailable records whether, at controller start-up,
// the ledger Credentials CRD was present so the reconciler could register a
// watch on it. When true, changes to the connectivity-<stack> Credentials
// (notably its status.phase flipping to Ready) re-trigger the owning
// Connectivity's reconcile.
ledgerCredentialsWatchAvailable bool

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find all reads of ledgerCredentialsWatchAvailable and any requeue fallback.
rg -n -C4 'ledgerCredentialsWatchAvailable' --type=go
rg -n -C3 'RequeueAfter' internal/resources/connectivities internal/core --type=go

Repository: formancehq/operator

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -u

echo "== git files matching init.go =="
git ls-files | rg 'internal/resources/connectivities/init.go|LedgerCredentials|credential' || true

echo "== search ledgerCredentialsWatchAvailable exact/name =="
rg -n -C3 'ledgerCredentialsWatchAvailable|LedgerCredentials|LedgerCredentialsPending|RequeueAfter' --glob '*.go' . || true

echo "== file line counts =="
for f in $(git ls-files | rg 'internal/resources/connectivities/init.go'); do
  wc -l "$f"
done

echo "== outline init.go =="
f="$(git ls-files | rg 'internal/resources/connectivities/init.go' | head -1)"
if [ -n "${f:-}" ]; then
  ast-grep outline "$f" || true
fi

Repository: formancehq/operator

Length of output: 35824


🏁 Script executed:

#!/bin/bash
set -u

echo "== internal/core/reconciler.go around PendingError handling =="
rg -n 'PendingError|PendingErrors|reconcileObject|Terminal|Requeue' internal/core/reconciler.go --type=go
sed -n '140,310p' internal/core/reconciler.go

echo "== exact read grep =="
python3 - <<'PY'
from pathlib import Path
for p in Path('.').rglob('*.go'):
    text = p.read_text(errors='ignore')
    if 'ledgerCredentialsWatchAvailable' in text:
        for i,line in enumerate(text.splitlines(),1):
            if 'ledgerCredentialsWatchAvailable' in line:
                print(f"{p}:{i}:{line}")
PY

echo "== programmatic invariant check =="
python3 - <<'PY'
import re
from pathlib import Path

text = Path('internal/resources/connectivities/init.go').read_text()
write_refs = [i for i,l in enumerate(text.splitlines(),1) if 'ledgerCredentialsWatchAvailable' in l]
read_refs = []
for m in re.finditer(r'\bledgerCredentialsWatchAvailable\b', text):
    line_num = text[:m.start()].count('\n') + 1
    line = text.splitlines()[line_num-1]
    if 'ledgerCredentialsWatchAvailable =' in line or 'ledgerCredentialsWatchAvailable = ' in line or line.strip().startswith('ledgerCredentialsWatchAvailable ='):
        continue
    read_refs.append((line_num, line.strip()))

print("write_refs=", write_refs)
print("read_refs=", read_refs)
print("no_reads=", len(read_refs) == 0)
PY

Repository: formancehq/operator

Length of output: 8177


🏁 Script executed:

#!/bin/bash
set -u
sed -n '310,380p' internal/core/reconciler.go
rg -n -C2 'IsPending|PendingError|PendingErrors' --glob '*.go' . || true

Repository: formancehq/operator

Length of output: 22747


Add a requeue fallback when the ledger Credentials watch is unavailable.

ledgerCredentialsWatchAvailable is only written by withLedgerCredentialsWatch; it is never read. When the Credentials CRD is absent, controller setup disables the watch, and Reconcile still returns NewPendingError() with LedgerCredentialsPending. Since the reconciler returns ctrl.Result{} for that failure, the Connectivity stays pending with no scheduled retry. Return a delayed Requeue here, or remove this flag if the watch is expected to always be available.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/resources/connectivities/init.go` around lines 85 - 91, Use
ledgerCredentialsWatchAvailable in Reconcile when handling
LedgerCredentialsPending: if the watch is unavailable, return a delayed requeue
result so pending Connectivity resources retry without CRD events; preserve the
existing NewPendingError behavior and watch-driven path when the flag is true.

Comment on lines +316 to +321
// Deletes use client.IgnoreNotFound so the helper is idempotent and safe to call
// on every reconcile while the gate stays closed (including when the resources
// were never created). The names/scopes mirror how they are provisioned in
// Reconcile: the delegated Connectivity is namespaced (name == namespace ==
// stack name) and the GatewayHTTPAPI is cluster-scoped ("<stack>-connectivity").
func teardownDelegated(ctx Context, stack *v1beta1.Stack, connectivity *v1beta1.Connectivity) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the stale name description in the teardown doc comment.

The comment states the delegated Connectivity has name == namespace == stack name. The code sets the name to connectivityDelegatedName ("connectivity") and only the namespace to the stack name.

📝 Proposed doc fix
-// were never created). The names/scopes mirror how they are provisioned in
-// Reconcile: the delegated Connectivity is namespaced (name == namespace ==
-// stack name) and the GatewayHTTPAPI is cluster-scoped ("<stack>-connectivity").
+// were never created). The names/scopes mirror how they are provisioned in
+// Reconcile: the delegated Connectivity is namespaced in the stack namespace
+// with the fixed name "connectivity", and the GatewayHTTPAPI is cluster-scoped
+// ("<stack>-connectivity").
📝 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
// Deletes use client.IgnoreNotFound so the helper is idempotent and safe to call
// on every reconcile while the gate stays closed (including when the resources
// were never created). The names/scopes mirror how they are provisioned in
// Reconcile: the delegated Connectivity is namespaced (name == namespace ==
// stack name) and the GatewayHTTPAPI is cluster-scoped ("<stack>-connectivity").
func teardownDelegated(ctx Context, stack *v1beta1.Stack, connectivity *v1beta1.Connectivity) error {
// Deletes use client.IgnoreNotFound so the helper is idempotent and safe to call
// on every reconcile while the gate stays closed (including when the resources
// were never created). The names/scopes mirror how they are provisioned in
// Reconcile: the delegated Connectivity is namespaced in the stack namespace
// with the fixed name "connectivity", and the GatewayHTTPAPI is cluster-scoped
// ("<stack>-connectivity").
func teardownDelegated(ctx Context, stack *v1beta1.Stack, connectivity *v1beta1.Connectivity) error {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/resources/connectivities/init.go` around lines 316 - 321, Update the
doc comment for teardownDelegated to state that the delegated Connectivity uses
connectivityDelegatedName ("connectivity") as its name and the stack name as its
namespace; retain the existing GatewayHTTPAPI naming and scoping description.

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

Development

Successfully merging this pull request may close these issues.

3 participants