Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e0059ec
feat(gateway): add GatewayGRPCAPI CRD for gRPC service exposure
gfyrag May 25, 2026
3971821
fix(e2e): increase chainsaw grpcapi deletion step timeout
gfyrag May 25, 2026
de1e325
feat: add Dockerfile and Pulumi deployment app
gfyrag May 28, 2026
c0fdbff
refactor(pulumi): take namespace as parameter instead of creating it
gfyrag May 28, 2026
6aa2687
fix: add -buildvcs=false to Dockerfile go build
gfyrag May 28, 2026
7f56f8c
feat(ledger): delegate v3 provisioning to ledger operator
flemzord Jul 14, 2026
ba358fc
fix(ledger): preserve replica settings for v3
flemzord Jul 14, 2026
abb0643
feat(gateway): integrate gRPC routing for Ledger v3
flemzord Jul 14, 2026
5740dcb
feat(ledger): configure v3 auth and monitoring
flemzord Jul 14, 2026
959063a
fix(ledger): preserve monitoring service name
flemzord Jul 14, 2026
122316d
fix(ledger): clear legacy readiness conditions for v3
flemzord Jul 14, 2026
7261997
chore: ignore local temporary files
flemzord Jul 14, 2026
062bea6
feat(ledger): configure v3 runtime resources and TLS
flemzord Jul 14, 2026
2d6e7de
feat(ledger): add v3 preview mode
flemzord Jul 14, 2026
3daf0df
feat(ledger): secure v3 cluster networking
flemzord Jul 15, 2026
1b0cb68
feat(ledger): add stack-targeted v3 configuration
flemzord Jul 15, 2026
b5bf838
feat(ledger): map topology spread setting to v3
flemzord Jul 15, 2026
9ca4316
fix(ledger): make v3 capability optional
flemzord Jul 15, 2026
9e85a4f
fix(operator): harden optional ledger v3 integration
flemzord Jul 15, 2026
cfbc18e
fix(ci): stabilize ledger integration tests
flemzord Jul 15, 2026
e605fad
fix(ledger): give ledger v3 raft TLS secret a dedicated name (#491)
Dav-14 Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ charts

# E2E test artifacts
tests/e2e/artifacts/

# Local scratch files
tmp/
34 changes: 34 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
FROM golang:1.26-alpine AS builder

RUN apk add --no-cache git

WORKDIR /src

COPY go.mod go.sum ./
RUN go mod download

COPY . .

ARG VERSION=latest
ARG COMMIT=unknown
ARG LICENCE_PUBLIC_KEY_B64=""

WORKDIR /src/cmd

RUN set -e; \
LDFLAGS="-X github.com/formancehq/operator/v3/cmd.Version=${VERSION} \
-X github.com/formancehq/operator/v3/cmd.BuildDate=$(date +%s) \
-X github.com/formancehq/operator/v3/cmd.Commit=${COMMIT}"; \
if [ -n "$LICENCE_PUBLIC_KEY_B64" ]; then \
LICENCE_PUBLIC_KEY="$(printf '%s' "$LICENCE_PUBLIC_KEY_B64" | base64 -d)"; \
LDFLAGS="${LDFLAGS} -X 'github.com/formancehq/go-libs/v5/pkg/authn/licence.formancePublicKey=${LICENCE_PUBLIC_KEY}'"; \
fi; \
CGO_ENABLED=0 go build -buildvcs=false -o /usr/bin/operator -ldflags="${LDFLAGS}" .

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
Comment on lines +28 to +34

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Run the operator as a non-root user.

The final image defaults to root, weakening containment if the operator is compromised. Add an unprivileged user and switch to it after copying the binary.

Proposed fix
 FROM alpine:3.20
 
 RUN apk update && apk add --no-cache ca-certificates curl
+RUN 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.

Suggested change
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
RUN 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, Update the final Docker image stage after
copying the operator binary to create an unprivileged user and configure the
image to run as that user, ensuring the existing ENTRYPOINT continues to invoke
/usr/bin/operator without root privileges.

Source: Linters/SAST tools

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] Operator container runs as root

The final image stage does not declare an unprivileged user, so the operator process runs as root. This weakens containment if the operator is ever compromised.

Suggestion: Add RUN addgroup -S operator && adduser -S -G operator operator after the apk install step and add USER operator after copying the binary.

10 changes: 7 additions & 3 deletions Earthfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,13 @@ compile:
SAVE ARTIFACT main

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 .
Comment on lines +48 to +54

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
awk '/^build-image:/{active=1} active && /ARG tag=/{declared=NR} active && /\$tag/{used=NR; exit} END {
  if (!declared || declared > used) exit 1
}' Earthfile

Repository: formancehq/operator

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '--- Earthfile excerpt ---\n'
nl -ba Earthfile | sed -n '35,70p'
printf '\n--- Search for ARG tag occurrences ---\n'
rg -n '^\s*ARG\s+tag(\s*=|$)|\$tag\b' Earthfile

Repository: formancehq/operator

Length of output: 248


🏁 Script executed:

#!/bin/bash
set -euo pipefail
nl -ba Earthfile | sed -n '35,70p'
rg -n '^\s*ARG\s+tag(\s*=|$)|\$tag\b' Earthfile

Repository: formancehq/operator

Length of output: 197


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- Earthfile excerpt (35-70) ---'
sed -n '35,70p' Earthfile | awk '{printf "%4d  %s\n", NR+34, $0}'

echo
echo '--- ARG tag / $tag references ---'
awk '
  /^\s*ARG\s+tag(\s*=|$)/ { print FNR ":" $0 }
  /\$tag\b/ { print FNR ":" $0 }
' Earthfile

Repository: formancehq/operator

Length of output: 1846


Declare tag before FROM DOCKERFILE. $tag is used before its ARG declaration, so VERSION can resolve to the empty/default 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 `@Earthfile` around lines 48 - 54, Declare the `tag` build argument before the
`FROM DOCKERFILE` statement that passes `$tag` as the `VERSION` build argument.
Keep the existing `LICENCE_PUBLIC_KEY_B64` and `EARTHLY_BUILD_SHA` declarations
and forwarding unchanged.

ARG REPOSITORY=ghcr.io
ARG tag=latest
DO --pass-args core+SAVE_IMAGE --COMPONENT=operator --TAG=$tag
Expand Down
6 changes: 6 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,10 @@ resources:
kind: Broker
path: github.com/formancehq/operator/api/formance.com/v1beta1
version: v1beta1
- api:
crdVersion: v1
group: formance.com
kind: LedgerConfiguration
path: github.com/formancehq/operator/api/formance.com/v1beta1
version: v1beta1
version: "3"
3 changes: 3 additions & 0 deletions api/formance.com/v1beta1/gateway_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ type GatewayStatus struct {
// Detected http apis. See [GatewayHTTPAPI](#gatewayhttpapi)
//+optional
SyncHTTPAPIs []string `json:"syncHTTPAPIs"`
// Detected grpc apis. See [GatewayGRPCAPI](#gatewaygrpcapi)
//+optional
SyncGRPCAPIs []string `json:"syncGRPCAPIs,omitempty"`
}

//+kubebuilder:object:root=true
Expand Down
48 changes: 48 additions & 0 deletions api/formance.com/v1beta1/gatewaybackend_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
Copyright 2022.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1beta1

const GatewayBackendTLSSecretLabel = "formance.com/gateway-backend-tls"

// GatewayBackendTLS configures TLS when Gateway connects to a backend.
type GatewayBackendTLS struct {
// SecretName contains the CA used to verify the backend certificate.
// +kubebuilder:validation:MinLength=1
SecretName string `json:"secretName"`
// CASecretKey is the key containing the CA certificate.
// +optional
// +kubebuilder:default:="ca.crt"
CASecretKey string `json:"caSecretKey,omitempty"`
// ServerName is used for backend certificate verification.
// +kubebuilder:validation:MinLength=1
ServerName string `json:"serverName"`
}

// GatewayBackendRef selects the Kubernetes Service used by a Gateway route.
// When omitted, Gateway keeps using the module's historical Service.
type GatewayBackendRef struct {
// Name is the backend Service name in the Stack namespace.
// +kubebuilder:validation:MinLength=1
Name string `json:"name"`
// Port is the backend Service port.
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=65535
Port int32 `json:"port"`
// TLS enables a verified TLS connection to the backend.
// +optional
TLS *GatewayBackendTLS `json:"tls,omitempty"`
}
91 changes: 91 additions & 0 deletions api/formance.com/v1beta1/gatewaygrpcapi_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
Copyright 2022.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1beta1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type GatewayGRPCAPISpec struct {
StackDependency `json:",inline"`
// Name indicates the module name (e.g. "ledger")
Name string `json:"name"`
// GRPCServices is the list of fully-qualified gRPC service names
// exposed by this module (e.g. "formance.ledger.v1.LedgerService")
GRPCServices []string `json:"grpcServices"`
// Port is the gRPC port on the backend service
//+optional
//+kubebuilder:default:=8081
Port int32 `json:"port,omitempty"`
// BackendRef overrides the historical <name>-grpc Service.
// +optional
BackendRef *GatewayBackendRef `json:"backendRef,omitempty"`
}

type GatewayGRPCAPIStatus struct {
Status `json:",inline"`
//+optional
Ready bool `json:"ready,omitempty"`
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:resource:scope=Cluster
//+kubebuilder:printcolumn:name="Stack",type=string,JSONPath=".spec.stack",description="Stack"
//+kubebuilder:printcolumn:name="Ready",type=string,JSONPath=".status.ready",description="Ready"

// GatewayGRPCAPI is the Schema for the GRPCAPIs API
type GatewayGRPCAPI struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec GatewayGRPCAPISpec `json:"spec,omitempty"`
Status GatewayGRPCAPIStatus `json:"status,omitempty"`
}

func (in *GatewayGRPCAPI) SetReady(b bool) {
in.Status.Ready = b
}

func (in *GatewayGRPCAPI) IsReady() bool {
return in.Status.Ready
}

func (in *GatewayGRPCAPI) SetError(s string) {
in.Status.Info = s
}

func (a GatewayGRPCAPI) GetStack() string {
return a.Spec.Stack
}

func (in *GatewayGRPCAPI) GetConditions() *Conditions {
return &in.Status.Conditions
}

//+kubebuilder:object:root=true

// GatewayGRPCAPIList contains a list of GatewayGRPCAPI
type GatewayGRPCAPIList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []GatewayGRPCAPI `json:"items"`
}

func init() {
SchemeBuilder.Register(&GatewayGRPCAPI{}, &GatewayGRPCAPIList{})
}
3 changes: 3 additions & 0 deletions api/formance.com/v1beta1/gatewayhttpapi_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ type GatewayHTTPAPIRule struct {
//+optional
//+kubebuilder:default:=false
Secured bool `json:"secured"`
// BackendRef overrides the historical module Service for this rule.
// +optional
BackendRef *GatewayBackendRef `json:"backendRef,omitempty"`
}

type GatewayHTTPAPISpec struct {
Expand Down
2 changes: 2 additions & 0 deletions api/formance.com/v1beta1/ledger_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const LedgerV3Label = "formance.com/ledger-v3"

type LedgerSpec struct {
ModuleProperties `json:",inline"`
StackDependency `json:",inline"`
Expand Down
70 changes: 70 additions & 0 deletions api/formance.com/v1beta1/ledgerconfiguration_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
Copyright 2023.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1beta1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

ledgerv1alpha1 "github.com/formancehq/ledger/misc/operator/api/v1alpha1"
)

const DefaultLedgerConfigurationName = "default"

type LedgerConfigurationSpec struct {
// Stacks on which the configuration is applied. Can contain `*` to
// indicate a wildcard, following the same convention as Settings.
// +optional
Stacks []string `json:"stacks,omitempty"`

// Cluster is the base Ledger v3 Cluster specification. Stack-specific
// Settings and values owned by the Operator are applied on top of it.
// +optional
Cluster ledgerv1alpha1.ClusterSpec `json:"cluster,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Cluster
// LedgerConfiguration defines the base specification applied to every Ledger v3
// Cluster targeted by spec.stacks. A configuration targeting a stack by name
// takes priority over a configuration targeting all stacks with `*`.
type LedgerConfiguration struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec LedgerConfigurationSpec `json:"spec,omitempty"`
}

func (in *LedgerConfiguration) GetStacks() []string {
return in.Spec.Stacks
}

func (in *LedgerConfiguration) IsWildcard() bool {
return len(in.Spec.Stacks) == 1 && in.Spec.Stacks[0] == "*"
}

// +kubebuilder:object:root=true

// LedgerConfigurationList contains a list of LedgerConfiguration
type LedgerConfigurationList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []LedgerConfiguration `json:"items"`
}

func init() {
SchemeBuilder.Register(&LedgerConfiguration{}, &LedgerConfigurationList{})
}
Loading
Loading