feat(gateway): add GatewayGRPCAPI CRD for gRPC service exposure#469
feat(gateway): add GatewayGRPCAPI CRD for gRPC service exposure#469gfyrag wants to merge 5 commits into
Conversation
|
Warning Review limit reached
More reviews will be available in 44 minutes and 13 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (18)
📒 Files selected for processing (18)
WalkthroughAdds a new ChangesgRPC API Gateway Support
Build & Deploy Tooling
🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested labels:
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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 `@api/formance.com/v1beta1/gatewaygrpcapi_types.go`:
- Around line 36-40: GatewayGRPCAPIStatus embeds Status which already contains
the ready field, so the duplicate Ready bool in GatewayGRPCAPIStatus causes
ambiguous JSON serialization; remove the Ready bool declaration from the
GatewayGRPCAPIStatus struct and rely on the embedded Status.ready instead, then
search for and update any callers that referenced GatewayGRPCAPIStatus.Ready to
use the embedded Status's readiness accessor or field (i.e., Status.ready or its
getter) to avoid breaking references.
- Around line 30-33: Add CRD numeric validation annotations to the Port field so
Kubernetes rejects out-of-range values; on the Port int32 field in the
GatewayGRPCAPI type (Port) add kubebuilder validation tags to enforce Minimum=1
and Maximum=65535 (e.g., +kubebuilder:validation:Minimum=1 and
+kubebuilder:validation:Maximum=65535) so spec.port maps to a valid Service port
range.
In `@docs/09-Configuration` reference/02-Custom Resource Definitions.md:
- Around line 2177-2180: The GatewayGRPCAPIStatus table contains a duplicated
`ready` row; remove the second redundant row (the third line in the shown diff)
so the schema only documents `ready` once, and ensure the remaining rows
correctly list `ready` _boolean_, `info` _string_, and any other unique status
fields under the GatewayGRPCAPIStatus section.
In `@internal/tests/gateway_controller_test.go`:
- Around line 290-292: The test is calling g.Expect(LoadResource("",
gateway.Name, gateway)) inside Eventually without an assertion, so LoadResource
failures can be ignored; update each call site (e.g., the Eventually closure
that references LoadResource, Gomega, gateway and gateway.Status.SyncGRPCAPIs)
to assert success by appending a matcher like .To(Succeed()) to the Expect call
(replace bare g.Expect(LoadResource(...)) with
g.Expect(LoadResource(...)).To(Succeed()) in the Eventually blocks and the other
mentioned lines).
🪄 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
Run ID: dbcfc20f-5754-4add-9bd5-60e758335d75
⛔ Files ignored due to path filters (15)
config/crd/bases/formance.com_gatewaygrpcapis.yamlis excluded by!**/*.yamlconfig/crd/bases/formance.com_gateways.yamlis excluded by!**/*.yamlconfig/crd/kustomization.yamlis excluded by!**/*.yamlconfig/rbac/role.yamlis excluded by!**/*.yamldocs/09-Configuration reference/settings.catalog.jsonis excluded by!**/*.jsonhelm/crds/templates/crds/apiextensions.k8s.io_v1_customresourcedefinition_gatewaygrpcapis.formance.com.yamlis excluded by!**/*.yamlhelm/crds/templates/crds/apiextensions.k8s.io_v1_customresourcedefinition_gateways.formance.com.yamlis excluded by!**/*.yamlhelm/operator/templates/gen/rbac.authorization.k8s.io_v1_clusterrole_formance-manager-role.yamlis excluded by!**/gen/**,!**/*.yaml,!**/gen/**internal/tests/testdata/resources/gateway-controller/configmap-with-ledger-and-grpc.yamlis excluded by!**/*.yamltests/e2e/chainsaw/26-gatewaygrpcapi-sync/chainsaw-test.yamlis excluded by!**/*.yamltests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/gateway.yamlis excluded by!**/*.yamltests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/grpcapi-updated.yamlis excluded by!**/*.yamltests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/grpcapi.yamlis excluded by!**/*.yamltests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/httpapi-ledger.yamlis excluded by!**/*.yamltests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/stack.yamlis excluded by!**/*.yaml
📒 Files selected for processing (13)
api/formance.com/v1beta1/gateway_types.goapi/formance.com/v1beta1/gatewaygrpcapi_types.goapi/formance.com/v1beta1/zz_generated.deepcopy.godocs/09-Configuration reference/02-Custom Resource Definitions.mdinternal/resources/all.gointernal/resources/gatewaygrpcapis/create.gointernal/resources/gatewaygrpcapis/init.gointernal/resources/gateways/Caddyfile.gotplinternal/resources/gateways/caddyfile.gointernal/resources/gateways/configuration.gointernal/resources/gateways/init.gointernal/tests/gateway_controller_test.gointernal/tests/gatewaygrpcapi_controller_test.go
| // Port is the gRPC port on the backend service | ||
| //+optional | ||
| //+kubebuilder:default:=8081 | ||
| Port int32 `json:"port,omitempty"` |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
What is the valid range for Kubernetes Service.spec.ports[].port?
💡 Result:
Kubernetes Service.spec.ports[].port is the TCP/UDP/SCTP service port exposed by the Service. Its valid range is 1–65535 (inclusive). [1] Kubernetes does not allow 0 for spec.ports[*].port; the API field is an integer port number exposed by the Service. [1]
Citations:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the file and show the relevant section
ls -la api/formance.com/v1beta1/gatewaygrpcapi_types.go >/dev/null 2>&1 || true
sed -n '1,120p' api/formance.com/v1beta1/gatewaygrpcapi_types.go | nl -ba
# Find all uses of the Port field (likely .Port or spec.port) across the repo
rg -n --hidden --glob '!*vendor/*' "Gateway.*grpc|gatewaygrpc|gatewaygrpcapi|spec\.port|\.Port\b|json:\"port\"|Port int32" .
# Look for existing kubebuilder numeric bound markers to match repo style
rg -n --hidden --glob '!*vendor/*' "kubebuilder:validation:(Minimum|Maximum):=" .Repository: formancehq/operator
Length of output: 106
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="api/formance.com/v1beta1/gatewaygrpcapi_types.go"
if [ -f "$FILE" ]; then
sed -n '1,120p' "$FILE" | sed -n '1,120p'
else
echo "Missing expected file: $FILE"
echo "Listing matching gatewaygrpcapi_types.go files:"
find . -name 'gatewaygrpcapi_types.go' -print
fi
# Show the exact Port field vicinity (more focused)
if [ -f "$FILE" ]; then
rg -n "Port int32" "$FILE" -n
sed -n '20,60p' "$FILE"
fi
# Find usages of this Port field / json:"port" tags / gateway grpc port conversion
rg -n --hidden --glob '!*vendor/*' --glob '!*dist/*' \
'json:"port"|spec\.port|\.Port\b|gatewaygrpcapi|Gatewaygrpc|grpc port|Service.*port|spec\.ports' .
# Find existing kubebuilder numeric bound markers in the repo
rg -n --hidden --glob '!*vendor/*' 'kubebuilder:validation:(Minimum|Maximum):=' .Repository: formancehq/operator
Length of output: 10858
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== CRD base schema (formance.com_gatewaygrpcapis.yaml) ==="
CRD="config/crd/bases/formance.com_gatewaygrpcapis.yaml"
if [ -f "$CRD" ]; then
# show only port schema area
rg -n "port" "$CRD" | head -n 80
# show likely properties section around "port"
sed -n '1,120p' "$CRD"
# try to locate port property block by searching for "port:" and show nearby lines
LINE=$(rg -n "port" "$CRD" | head -n 1 | cut -d: -f1 || true)
if [ -n "${LINE:-}" ]; then
START=$((LINE-20)); END=$((LINE+60))
sed -n "${START},${END}p" "$CRD"
fi
else
echo "Missing $CRD"
fi
echo "=== Helm CRD template ==="
HELM_CRD="helm/crds/templates/crds/apiextensions.k8s.io_v1_customresourcedefinition_gatewaygrpcapis.formance.com.yaml"
if [ -f "$HELM_CRD" ]; then
rg -n "port" "$HELM_CRD" | head -n 80
LINE=$(rg -n "port" "$HELM_CRD" | head -n 1 | cut -d: -f1 || true)
if [ -n "${LINE:-}" ]; then
START=$((LINE-20)); END=$((LINE+60))
sed -n "${START},${END}p" "$HELM_CRD"
fi
else
echo "Missing $HELM_CRD"
fi
echo "=== Code usage ==="
echo "--- internal/resources/gatewaygrpcapis/create.go (port setting) ---"
sed -n '1,120p' internal/resources/gatewaygrpcapis/create.go
echo "--- internal/resources/gatewaygrpcapis/init.go (service creation input) ---"
sed -n '1,120p' internal/resources/gatewaygrpcapis/init.go
echo "--- internal/resources/services/services.go (service spec ports) ---"
sed -n '1,120p' internal/resources/services/services.go
echo "=== Any explicit handling of port==0 ==="
rg -n --hidden --glob '!*vendor/*' --glob '!*dist/*' '== *0|<= *0|minimum|maximum|Port[^A-Za-z_]|cfg\.Port|Spec\.Port' internal | head -n 200Repository: formancehq/operator
Length of output: 26154
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n --hidden --glob '!*vendor/*' 'kubebuilder:validation:' .
rg -n --hidden --glob '!*vendor/*' 'kubebuilder:validation:(Minimum|Maximum):=' .
rg -n --hidden --glob '!*vendor/*' 'kubebuilder:validation:(ExclusiveMinimum|ExclusiveMaximum):=' .Repository: formancehq/operator
Length of output: 1465
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== shared.go kubebuilder validation section (around Minimum=0 etc.) ==="
sed -n '40,120p' api/formance.com/v1beta1/shared.go | cat
echo "=== Any Maximum= / Minimum= markers in repo ==="
rg -n --hidden --glob '!*vendor/*' 'kubebuilder:validation:(Minimum|Maximum)=' api/formance.com/v1beta1Repository: formancehq/operator
Length of output: 3943
Add CRD validation bounds for spec.port
GatewayGRPCAPI.spec.port is fed directly into Service.spec.ports[].port, but the generated CRD schema currently imposes no numeric bounds—so values like 0 can be admitted even though Kubernetes Service ports must be in the range 1–65535.
Suggested fix
// Port is the gRPC port on the backend service
//+optional
//+kubebuilder:default:=8081
+ //+kubebuilder:validation:Minimum=1
+ //+kubebuilder:validation:Maximum=65535
Port int32 `json:"port,omitempty"`📝 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.
| // Port is the gRPC port on the backend service | |
| //+optional | |
| //+kubebuilder:default:=8081 | |
| Port int32 `json:"port,omitempty"` | |
| // Port is the gRPC port on the backend service | |
| //+optional | |
| //+kubebuilder:default:=8081 | |
| //+kubebuilder:validation:Minimum=1 | |
| //+kubebuilder:validation:Maximum=65535 | |
| Port int32 `json:"port,omitempty"` |
🤖 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 `@api/formance.com/v1beta1/gatewaygrpcapi_types.go` around lines 30 - 33, Add
CRD numeric validation annotations to the Port field so Kubernetes rejects
out-of-range values; on the Port int32 field in the GatewayGRPCAPI type (Port)
add kubebuilder validation tags to enforce Minimum=1 and Maximum=65535 (e.g.,
+kubebuilder:validation:Minimum=1 and +kubebuilder:validation:Maximum=65535) so
spec.port maps to a valid Service port range.
| type GatewayGRPCAPIStatus struct { | ||
| Status `json:",inline"` | ||
| //+optional | ||
| Ready bool `json:"ready,omitempty"` | ||
| } |
There was a problem hiding this comment.
Remove duplicate ready status field to avoid ambiguous serialization.
GatewayGRPCAPIStatus already embeds Status (which contains ready). Re-declaring Ready here creates two fields for the same JSON key and can desync readiness reads/writes.
Suggested fix
type GatewayGRPCAPIStatus struct {
Status `json:",inline"`
- //+optional
- Ready bool `json:"ready,omitempty"`
}📝 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.
| type GatewayGRPCAPIStatus struct { | |
| Status `json:",inline"` | |
| //+optional | |
| Ready bool `json:"ready,omitempty"` | |
| } | |
| type GatewayGRPCAPIStatus struct { | |
| Status `json:",inline"` | |
| } |
🤖 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 `@api/formance.com/v1beta1/gatewaygrpcapi_types.go` around lines 36 - 40,
GatewayGRPCAPIStatus embeds Status which already contains the ready field, so
the duplicate Ready bool in GatewayGRPCAPIStatus causes ambiguous JSON
serialization; remove the Ready bool declaration from the GatewayGRPCAPIStatus
struct and rely on the embedded Status.ready instead, then search for and update
any callers that referenced GatewayGRPCAPIStatus.Ready to use the embedded
Status's readiness accessor or field (i.e., Status.ready or its getter) to avoid
breaking references.
| | `ready` _boolean_ | Ready indicates if the resource is seen as completely reconciled | | | | ||
| | `info` _string_ | Info can contain any additional like reconciliation errors | | | | ||
| | `ready` _boolean_ | | | | | ||
|
|
There was a problem hiding this comment.
Remove duplicated ready field row in GatewayGRPCAPIStatus table.
Line 2179 repeats the ready field already documented on Line 2177, which makes the schema table inconsistent.
📝 Proposed fix
| `ready` _boolean_ | Ready indicates if the resource is seen as completely reconciled | | |
| `info` _string_ | Info can contain any additional like reconciliation errors | | |
-| `ready` _boolean_ | | | |📝 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.
| | `ready` _boolean_ | Ready indicates if the resource is seen as completely reconciled | | | | |
| | `info` _string_ | Info can contain any additional like reconciliation errors | | | | |
| | `ready` _boolean_ | | | | | |
| | `ready` _boolean_ | Ready indicates if the resource is seen as completely reconciled | | | | |
| | `info` _string_ | Info can contain any additional like reconciliation errors | | | |
🤖 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 2177 - 2180, The GatewayGRPCAPIStatus table contains a duplicated `ready`
row; remove the second redundant row (the third line in the shown diff) so the
schema only documents `ready` once, and ensure the remaining rows correctly list
`ready` _boolean_, `info` _string_, and any other unique status fields under the
GatewayGRPCAPIStatus section.
| Eventually(func(g Gomega) []string { | ||
| g.Expect(LoadResource("", gateway.Name, gateway)) | ||
| return gateway.Status.SyncGRPCAPIs |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -nP 'g\.Expect\(LoadResource\([^)]*\)\)\s*$' --type goRepository: formancehq/operator
Length of output: 365
🏁 Script executed:
#!/bin/bash
rg -nP 'g\.Expect\(LoadResource\([^)]*\)\)\s*$' --type goRepository: formancehq/operator
Length of output: 365
🏁 Script executed:
#!/bin/bash
rg -nP 'g\.Expect\(LoadResource\([^)]*\)\)\s*$' --type goRepository: formancehq/operator
Length of output: 365
🏁 Script executed:
#!/bin/bash
rg -nP 'g\.Expect\(LoadResource\([^)]*\)\)\s*$' --type goRepository: formancehq/operator
Length of output: 365
🏁 Script executed:
#!/bin/bash
rg -nP 'g\.Expect\(LoadResource\([^)]*\)\)\s*$' --type goRepository: formancehq/operator
Length of output: 365
🏁 Script executed:
#!/bin/bash
rg -nP 'g\.Expect\(LoadResource\([^)]*\)\)\s*$' --type goRepository: formancehq/operator
Length of output: 365
🏁 Script executed:
#!/bin/bash
rg -nP 'g\.Expect\(LoadResource\([^)]*\)\)\s*$' --type goRepository: formancehq/operator
Length of output: 365
🏁 Script executed:
#!/bin/bash
rg -nP 'g\.Expect\(LoadResource\([^)]*\)\)\s*$' --type goRepository: formancehq/operator
Length of output: 365
🏁 Script executed:
#!/bin/bash
rg -nP 'g\.Expect\(LoadResource\([^)]*\)\)\s*$' --type goRepository: formancehq/operator
Length of output: 365
🏁 Script executed:
#!/bin/bash
rg -nP 'g\.Expect\(LoadResource\([^)]*\)\)\s*$' --type goRepository: formancehq/operator
Length of output: 365
🏁 Script executed:
#!/bin/bash
rg -nP 'g\.Expect\(LoadResource\([^)]*\)\)\s*$' --type goRepository: formancehq/operator
Length of output: 365
🏁 Script executed:
#!/bin/bash
rg -nP 'g\.Expect\(LoadResource\([^)]*\)\)\s*$' --type goRepository: formancehq/operator
Length of output: 365
🏁 Script executed:
#!/bin/bash
rg -nP 'g\.Expect\(LoadResource\([^)]*\)\)\s*$' --type goRepository: formancehq/operator
Length of output: 365
🏁 Script executed:
#!/bin/bash
rg -nP 'g\.Expect\(LoadResource\([^)]*\)\)\s*$' --type goRepository: formancehq/operator
Length of output: 365
Assert LoadResource failures inside Eventually (and other call sites).
internal/tests/gateway_controller_test.go has matcher-less g.Expect(LoadResource("", gateway.Name, gateway)) at lines 91, 258, and 291, so LoadResource errors aren’t asserted during polling and can be masked. Change each to assert success (e.g., .To(Succeed())).
✅ Proposed fix (gRPC `Eventually` case around lines 290-292)
Eventually(func(g Gomega) []string {
- g.Expect(LoadResource("", gateway.Name, gateway))
+ g.Expect(LoadResource("", gateway.Name, gateway)).To(Succeed())
return gateway.Status.SyncGRPCAPIs
}).Should(ContainElements(grpcAPI.Spec.Name))📝 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.
| Eventually(func(g Gomega) []string { | |
| g.Expect(LoadResource("", gateway.Name, gateway)) | |
| return gateway.Status.SyncGRPCAPIs | |
| Eventually(func(g Gomega) []string { | |
| g.Expect(LoadResource("", gateway.Name, gateway)).To(Succeed()) | |
| return gateway.Status.SyncGRPCAPIs |
🤖 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/gateway_controller_test.go` around lines 290 - 292, The test
is calling g.Expect(LoadResource("", gateway.Name, gateway)) inside Eventually
without an assertion, so LoadResource failures can be ignored; update each call
site (e.g., the Eventually closure that references LoadResource, Gomega, gateway
and gateway.Status.SyncGRPCAPIs) to assert success by appending a matcher like
.To(Succeed()) to the Expect call (replace bare g.Expect(LoadResource(...)) with
g.Expect(LoadResource(...)).To(Succeed()) in the Eventually blocks and the other
mentioned lines).
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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 34-49: After calling filepath.Glob to populate crdFiles, add an
explicit empty-check and return an error if no files were found: check
len(crdFiles) == 0 and return a descriptive fmt.Errorf (e.g., "no CRD files
found in config/crd/bases") before proceeding to the loop that creates resources
(the block using variables crdFiles, name, and k8syaml.NewConfigFile); this
ensures the function fails fast when no CRDs are discovered.
- Around line 63-73: The code reads the secret `licence-token` using cfg.Get and
wraps it with pulumi.String, which bypasses Pulumi secret propagation; change
the retrieval to use cfg.GetSecret or cfg.RequireSecret to get a pulumi.Secret
Output and pass that Output directly into the Helm values map (the
`licenceValues` / `"token"` entry) instead of converting to a plain string so
the secret remains encrypted/redacted in state and outputs.
In `@Dockerfile`:
- Around line 28-34: The runtime stage currently runs as root; create a
dedicated non-root user/group (e.g., operator:operator) in the final stage,
chown the installed binary (/usr/bin/operator) to that user, and add a USER
operator line before ENTRYPOINT so the container drops root privileges;
reference the final-stage Dockerfile lines that copy the binary (COPY
--from=builder /usr/bin/operator /usr/bin/operator) and the ENTRYPOINT
["/usr/bin/operator"] when making the changes.
🪄 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
Run ID: f5e43e92-db75-4fdf-b8db-6c4fa0e40f5e
⛔ Files ignored due to path filters (3)
deployment/operator/Pulumi.yamlis excluded by!**/*.yamldeployment/operator/go.modis excluded by!**/*.moddeployment/operator/go.sumis excluded by!**/*.sum,!**/*.sum
📒 Files selected for processing (5)
DockerfileEarthfiledeployment/operator/.gitignoredeployment/operator/helpers.godeployment/operator/main.go
✅ Files skipped from review due to trivial changes (1)
- deployment/operator/.gitignore
| crdFiles, err := filepath.Glob(filepath.Join("..", "..", "config", "crd", "bases", "*.yaml")) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to glob CRD files: %w", err) | ||
| } | ||
|
|
||
| var crds []pulumi.Resource | ||
| for _, crdFile := range crdFiles { | ||
| name := strings.TrimSuffix(filepath.Base(crdFile), filepath.Ext(crdFile)) | ||
| crd, crdErr := k8syaml.NewConfigFile(ctx, name+"-crd", &k8syaml.ConfigFileArgs{ | ||
| File: crdFile, | ||
| }, pulumi.Provider(k8sProvider)) | ||
| if crdErr != nil { | ||
| return fmt.Errorf("failed to apply CRD %s: %w", name, crdErr) | ||
| } | ||
| crds = append(crds, crd) | ||
| } |
There was a problem hiding this comment.
Fail fast when no CRD files are discovered.
If the CRD glob resolves to zero files, deployment continues, but Helm CRD creation is disabled later. Add an explicit empty-check and return an error.
Suggested fix
crdFiles, err := filepath.Glob(filepath.Join("..", "..", "config", "crd", "bases", "*.yaml"))
if err != nil {
return fmt.Errorf("failed to glob CRD files: %w", err)
}
+ if len(crdFiles) == 0 {
+ return fmt.Errorf("no CRD files found in config/crd/bases; refusing to deploy with operator-crds.create=false")
+ }
var crds []pulumi.Resource📝 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.
| crdFiles, err := filepath.Glob(filepath.Join("..", "..", "config", "crd", "bases", "*.yaml")) | |
| if err != nil { | |
| return fmt.Errorf("failed to glob CRD files: %w", err) | |
| } | |
| var crds []pulumi.Resource | |
| for _, crdFile := range crdFiles { | |
| name := strings.TrimSuffix(filepath.Base(crdFile), filepath.Ext(crdFile)) | |
| crd, crdErr := k8syaml.NewConfigFile(ctx, name+"-crd", &k8syaml.ConfigFileArgs{ | |
| File: crdFile, | |
| }, pulumi.Provider(k8sProvider)) | |
| if crdErr != nil { | |
| return fmt.Errorf("failed to apply CRD %s: %w", name, crdErr) | |
| } | |
| crds = append(crds, crd) | |
| } | |
| crdFiles, err := filepath.Glob(filepath.Join("..", "..", "config", "crd", "bases", "*.yaml")) | |
| if err != nil { | |
| return fmt.Errorf("failed to glob CRD files: %w", err) | |
| } | |
| if len(crdFiles) == 0 { | |
| return fmt.Errorf("no CRD files found in config/crd/bases; refusing to deploy with operator-crds.create=false") | |
| } | |
| var crds []pulumi.Resource | |
| for _, crdFile := range crdFiles { | |
| name := strings.TrimSuffix(filepath.Base(crdFile), filepath.Ext(crdFile)) | |
| crd, crdErr := k8syaml.NewConfigFile(ctx, name+"-crd", &k8syaml.ConfigFileArgs{ | |
| File: crdFile, | |
| }, pulumi.Provider(k8sProvider)) | |
| if crdErr != nil { | |
| return fmt.Errorf("failed to apply CRD %s: %w", name, crdErr) | |
| } | |
| crds = append(crds, crd) | |
| } |
🤖 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 34 - 49, After calling
filepath.Glob to populate crdFiles, add an explicit empty-check and return an
error if no files were found: check len(crdFiles) == 0 and return a descriptive
fmt.Errorf (e.g., "no CRD files found in config/crd/bases") before proceeding to
the loop that creates resources (the block using variables crdFiles, name, and
k8syaml.NewConfigFile); this ensures the function fails fast when no CRDs are
discovered.
| licenceToken := cfg.Get("licence-token") | ||
| if licenceToken != "" { | ||
| licenceIssuer := cfg.Get("licence-issuer") | ||
| if licenceIssuer == "" { | ||
| licenceIssuer = "https://license.formance.cloud/keys" | ||
| } | ||
| licenceValues = pulumi.Map{ | ||
| "createSecret": pulumi.Bool(true), | ||
| "token": pulumi.String(licenceToken), | ||
| "issuer": pulumi.String(licenceIssuer), | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Pulumi Go SDK: what is the difference between config.Config.Get and config.Config.GetSecret for secret config values, and how does each affect state redaction?
💡 Result:
In the Pulumi Go SDK, the primary difference between config.Config.Get and config.Config.GetSecret lies in how the retrieved value is handled by the Pulumi engine regarding state encryption and redaction [1][2]. config.Config.Get This method retrieves a configuration value as a plain string (or other standard type) [3][2]. If the underlying value in your configuration is encrypted (a secret), using Get will decrypt it for use in your program [1]. However, because it returns a plain value rather than an Output, Pulumi cannot track the "secret-ness" of that data through your program [1][2]. Consequently, any resource inputs or outputs derived from this value may be stored in plaintext in your state file and could appear in plain text in terminal output or logs [1][4]. config.Config.GetSecret This method retrieves a configuration value and returns it wrapped in a Pulumi Output (e.g., pulumi.StringOutput) [5][3]. This Output object carries metadata indicating that the underlying value is sensitive [2][4]. Effect on State Redaction The critical advantage of GetSecret is that it enables Pulumi's automated secrets propagation [4]: 1. Encryption in State: Because the value is wrapped in a secret Output, the Pulumi engine knows to treat it as sensitive [2]. If this value is passed to a resource property, Pulumi will encrypt it before storing it in the stack's state file [4][6]. 2. Propagation: Any values derived from this Output (such as strings concatenated with the secret) are also automatically marked as secrets, ensuring the sensitive nature of the data is maintained throughout your infrastructure graph [4]. 3. Redaction in CLI/Logs: Pulumi will automatically redact the value in CLI output, logs, and other diagnostic information, replacing it with placeholders like [secret] [4][7]. Best Practice Even if a configuration value is stored as a secret, you should always access it using GetSecret or RequireSecret [1]. Using standard getters like Get is discouraged because it breaks the tracking of sensitive data, leading to potential accidental exposure in your state files and logs [1].
Citations:
- 1: https://www.pulumi.com/docs/iac/concepts/secrets/
- 2: https://www.pulumi.com/docs/iac/concepts/config/
- 3: https://pkg.go.dev/github.com/pulumi/pulumi/sdk/v3/go/pulumi/config
- 4: https://infisical.com/blog/pulumi-secrets-management
- 5: https://pkg.go.dev/github.com/pulumi/pulumi/sdk/go/pulumi/config
- 6: https://www.pulumi.com/tutorials/building-with-pulumi/secrets/
- 7: pulumi/pulumi@78ccd92
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="deployment/operator/main.go"
echo "== File =="
ls -la "$FILE"
echo "== Lines 1-140 =="
nl -ba "$FILE" | sed -n '1,140p'
echo "== Search for licence-token and CRD glob =="
rg -n --context=2 "licence-token|licenceValues|crd|glob|operator-crds\.create|Create.*CRD" "$FILE" || true
echo "== Lines 40-120 =="
nl -ba "$FILE" | sed -n '40,120p'
echo "== Lines 120-220 =="
nl -ba "$FILE" | sed -n '120,220p'Repository: formancehq/operator
Length of output: 230
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="deployment/operator/main.go"
echo "== File =="
ls -la "$FILE"
echo "== Lines 1-220 =="
awk '{printf "%5d\t%s\n", NR, $0}' "$FILE" | sed -n '1,220p'
echo "== Search for relevant tokens =="
rg -n --context=2 "licence-token|licenceValues|licenceIssuer|operator-crds\.create|crd|glob|Create.*CRD" "$FILE" || trueRepository: formancehq/operator
Length of output: 6509
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Check CRD glob directory contents =="
ls -la config/crd/bases 2>/dev/null || true
echo "== Count CRD base YAMLs (for glob '..' '..' config/crd/bases/*.yaml relative to deployment/operator/) =="
# run from deployment/operator so relative paths match the code
cd deployment/operator
echo "CWD: $(pwd)"
echo "Glob: ../../config/crd/bases/*.yaml"
shopt -s nullglob
files=(../../config/crd/bases/*.yaml)
echo "Matches: ${`#files`[@]}"
printf '%s\n' "${files[@]}" | head -n 50
echo "== Search for licence-token secret-aware access =="
cd ..
rg -n --hidden --no-ignore-vcs "licence-token|GetSecret\\(\"licence-token\"\\)|RequireSecret\\(\"licence-token\"\\)" -S . || trueRepository: formancehq/operator
Length of output: 3886
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show Pulumi.yaml around licence-token config
FILE="deployment/operator/Pulumi.yaml"
if [ -f "$FILE" ]; then
echo "== Contents: $FILE (lines 1-120) =="
awk 'NR>=1 && NR<=140 {printf "%5d\t%s\n", NR, $0}' "$FILE"
fi
echo "== Search for other secret-aware config accessors in repo =="
rg -n --hidden --no-ignore-vcs "GetSecret\\(|RequireSecret\\(|secret\\s*:\\s*true" . || true
echo "== Show remaining helper usage in main.go (tail) =="
awk 'NR>=120 && NR<=220 {printf "%5d\t%s\n", NR, $0}' deployment/operator/main.goRepository: formancehq/operator
Length of output: 3108
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n --hidden --no-ignore-vcs "licence-token" . || true
rg -n --hidden --no-ignore-vcs "licenceValues|licenceIssuer|createSecret" deployment/operator -S || true
# show the exact helpers for getImagePullSecrets / config helpers
sed -n '1,220p' deployment/operator/helpers.goRepository: formancehq/operator
Length of output: 6655
Use secret-aware config access for licence-token.
deployment/operator/Pulumi.yaml marks licence-token as secret: true, but deployment/operator/main.go reads it with cfg.Get("licence-token") and passes it into Helm values as pulumi.String(licenceToken). This bypasses Pulumi secret propagation/redaction and can leak the token into stack state/outputs. Use cfg.GetSecret/cfg.RequireSecret and pass the secret Output directly to the Helm global.licence.token value.
🤖 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 63 - 73, The code reads the secret
`licence-token` using cfg.Get and wraps it with pulumi.String, which bypasses
Pulumi secret propagation; change the retrieval to use cfg.GetSecret or
cfg.RequireSecret to get a pulumi.Secret Output and pass that Output directly
into the Helm values map (the `licenceValues` / `"token"` entry) instead of
converting to a plain string so the secret remains encrypted/redacted in state
and outputs.
| FROM alpine:3.20 | ||
|
|
||
| RUN apk update && apk add --no-cache ca-certificates curl | ||
|
|
||
| ENTRYPOINT ["/usr/bin/operator"] | ||
|
|
||
| COPY --from=builder /usr/bin/operator /usr/bin/operator |
There was a problem hiding this comment.
Run the runtime container as a non-root user.
The final image runs as root by default. Please create a dedicated user/group and switch to it in the runtime stage.
Suggested fix
FROM alpine:3.20
-RUN apk update && apk add --no-cache ca-certificates curl
+RUN apk update && apk add --no-cache ca-certificates curl \
+ && addgroup -S operator \
+ && adduser -S -G operator operator
ENTRYPOINT ["/usr/bin/operator"]
COPY --from=builder /usr/bin/operator /usr/bin/operator
+USER operator📝 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.
| FROM alpine:3.20 | |
| RUN apk update && apk add --no-cache ca-certificates curl | |
| ENTRYPOINT ["/usr/bin/operator"] | |
| COPY --from=builder /usr/bin/operator /usr/bin/operator | |
| FROM alpine:3.20 | |
| RUN apk update && apk add --no-cache ca-certificates curl \ | |
| && addgroup -S operator \ | |
| && adduser -S -G operator operator | |
| ENTRYPOINT ["/usr/bin/operator"] | |
| COPY --from=builder /usr/bin/operator /usr/bin/operator | |
| USER operator |
🤖 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 `@Dockerfile` around lines 28 - 34, The runtime stage currently runs as root;
create a dedicated non-root user/group (e.g., operator:operator) in the final
stage, chown the installed binary (/usr/bin/operator) to that user, and add a
USER operator line before ENTRYPOINT so the container drops root privileges;
reference the final-stage Dockerfile lines that copy the binary (COPY
--from=builder /usr/bin/operator /usr/bin/operator) and the ENTRYPOINT
["/usr/bin/operator"] when making the changes.
Introduce a new GatewayGRPCAPI custom resource that allows modules to expose gRPC services through the gateway, following the same pattern as GatewayHTTPAPI. gRPC routing uses fully-qualified protobuf service names (e.g. formance.ledger.v1.LedgerService) instead of HTTP path prefixes. Traffic is served on the same port (8080) with h2c protocol enabled conditionally when gRPC APIs are registered. Each GatewayGRPCAPI creates a dedicated <name>-grpc Kubernetes Service pointing to the module's gRPC port, avoiding conflicts with existing HTTP services. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The deletion step was timing out on K8s 1.34 because the script timeout (2m) was being consumed by kubectl wait --for=delete. Increase the script timeout to 5m and the wait to 3m for more headroom. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract the operator image build from Earthly into a standard Dockerfile so it can be used by both Earthly (via FROM DOCKERFILE) and the new Pulumi deployment app. The Pulumi app (deployment/operator/) builds the operator image, applies CRDs, and deploys the operator via its Helm chart. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Helm release creates the namespace itself via createNamespace. Defaults to formance-system. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4557660 to
c0fdbff
Compare
| build-image: | ||
| FROM core+final-image | ||
| ENTRYPOINT ["/usr/bin/operator"] | ||
| COPY --pass-args (+compile/main) /usr/bin/operator | ||
| ARG LICENCE_PUBLIC_KEY_B64="" | ||
| ARG EARTHLY_BUILD_SHA | ||
| FROM DOCKERFILE \ | ||
| --build-arg LICENCE_PUBLIC_KEY_B64=$LICENCE_PUBLIC_KEY_B64 \ | ||
| --build-arg VERSION=$tag \ | ||
| --build-arg COMMIT=$EARTHLY_BUILD_SHA \ | ||
| -f Dockerfile . | ||
| ARG REPOSITORY=ghcr.io | ||
| ARG tag=latest | ||
| DO --pass-args core+SAVE_IMAGE --COMPONENT=operator --TAG=$tag |
There was a problem hiding this comment.
OH we still have a earthfile here
There was a problem hiding this comment.
Unfortunately yes ^^
The Docker build context may not include .git, causing go build to fail with "error obtaining VCS status". Disable VCS stamping since version info is already passed via ldflags. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
GatewayGRPCAPICRD that allows modules to expose gRPC services through the gatewayformance.ledger.v1.LedgerService) instead of HTTP path prefixes (/api/<module>)h2cprotocol enabled conditionally when gRPC APIs existGatewayGRPCAPIcreates a dedicated<name>-grpcKubernetes Service to avoid conflicts with HTTP servicesUsage
Test plan
26-gatewaygrpcapi-sync) covering create/mutate/delete lifecycle🤖 Generated with Claude Code