diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 000000000..6733ff7d3
--- /dev/null
+++ b/Dockerfile
@@ -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
diff --git a/Earthfile b/Earthfile
index bae9f52a9..34b69c286 100644
--- a/Earthfile
+++ b/Earthfile
@@ -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 .
ARG REPOSITORY=ghcr.io
ARG tag=latest
DO --pass-args core+SAVE_IMAGE --COMPONENT=operator --TAG=$tag
diff --git a/api/formance.com/v1beta1/gateway_types.go b/api/formance.com/v1beta1/gateway_types.go
index 5828da2a7..836fff3fb 100644
--- a/api/formance.com/v1beta1/gateway_types.go
+++ b/api/formance.com/v1beta1/gateway_types.go
@@ -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
diff --git a/api/formance.com/v1beta1/gatewaygrpcapi_types.go b/api/formance.com/v1beta1/gatewaygrpcapi_types.go
new file mode 100644
index 000000000..760ba134f
--- /dev/null
+++ b/api/formance.com/v1beta1/gatewaygrpcapi_types.go
@@ -0,0 +1,88 @@
+/*
+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"`
+}
+
+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{})
+}
diff --git a/api/formance.com/v1beta1/zz_generated.deepcopy.go b/api/formance.com/v1beta1/zz_generated.deepcopy.go
index 552862474..cafd67bb6 100644
--- a/api/formance.com/v1beta1/zz_generated.deepcopy.go
+++ b/api/formance.com/v1beta1/zz_generated.deepcopy.go
@@ -965,6 +965,102 @@ func (in *Gateway) DeepCopyObject() runtime.Object {
return nil
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GatewayGRPCAPI) DeepCopyInto(out *GatewayGRPCAPI) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ in.Spec.DeepCopyInto(&out.Spec)
+ in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayGRPCAPI.
+func (in *GatewayGRPCAPI) DeepCopy() *GatewayGRPCAPI {
+ if in == nil {
+ return nil
+ }
+ out := new(GatewayGRPCAPI)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *GatewayGRPCAPI) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GatewayGRPCAPIList) DeepCopyInto(out *GatewayGRPCAPIList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]GatewayGRPCAPI, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayGRPCAPIList.
+func (in *GatewayGRPCAPIList) DeepCopy() *GatewayGRPCAPIList {
+ if in == nil {
+ return nil
+ }
+ out := new(GatewayGRPCAPIList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *GatewayGRPCAPIList) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GatewayGRPCAPISpec) DeepCopyInto(out *GatewayGRPCAPISpec) {
+ *out = *in
+ out.StackDependency = in.StackDependency
+ if in.GRPCServices != nil {
+ in, out := &in.GRPCServices, &out.GRPCServices
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayGRPCAPISpec.
+func (in *GatewayGRPCAPISpec) DeepCopy() *GatewayGRPCAPISpec {
+ if in == nil {
+ return nil
+ }
+ out := new(GatewayGRPCAPISpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GatewayGRPCAPIStatus) DeepCopyInto(out *GatewayGRPCAPIStatus) {
+ *out = *in
+ in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayGRPCAPIStatus.
+func (in *GatewayGRPCAPIStatus) DeepCopy() *GatewayGRPCAPIStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(GatewayGRPCAPIStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GatewayHTTPAPI) DeepCopyInto(out *GatewayHTTPAPI) {
*out = *in
@@ -1198,6 +1294,11 @@ func (in *GatewayStatus) DeepCopyInto(out *GatewayStatus) {
*out = make([]string, len(*in))
copy(*out, *in)
}
+ if in.SyncGRPCAPIs != nil {
+ in, out := &in.SyncGRPCAPIs, &out.SyncGRPCAPIs
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayStatus.
diff --git a/config/crd/bases/formance.com_gatewaygrpcapis.yaml b/config/crd/bases/formance.com_gatewaygrpcapis.yaml
new file mode 100644
index 000000000..0145e7e0a
--- /dev/null
+++ b/config/crd/bases/formance.com_gatewaygrpcapis.yaml
@@ -0,0 +1,140 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.18.0
+ name: gatewaygrpcapis.formance.com
+spec:
+ group: formance.com
+ names:
+ kind: GatewayGRPCAPI
+ listKind: GatewayGRPCAPIList
+ plural: gatewaygrpcapis
+ singular: gatewaygrpcapi
+ scope: Cluster
+ versions:
+ - additionalPrinterColumns:
+ - description: Stack
+ jsonPath: .spec.stack
+ name: Stack
+ type: string
+ - description: Ready
+ jsonPath: .status.ready
+ name: Ready
+ type: string
+ name: v1beta1
+ schema:
+ openAPIV3Schema:
+ description: GatewayGRPCAPI is the Schema for the GRPCAPIs API
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ properties:
+ grpcServices:
+ description: |-
+ GRPCServices is the list of fully-qualified gRPC service names
+ exposed by this module (e.g. "formance.ledger.v1.LedgerService")
+ items:
+ type: string
+ type: array
+ name:
+ description: Name indicates the module name (e.g. "ledger")
+ type: string
+ port:
+ default: 8081
+ description: Port is the gRPC port on the backend service
+ format: int32
+ type: integer
+ stack:
+ description: Stack indicates the stack on which the module is installed
+ type: string
+ required:
+ - grpcServices
+ - name
+ type: object
+ status:
+ properties:
+ conditions:
+ items:
+ description: Condition contains details for one aspect of the current
+ state of this API Resource.
+ properties:
+ lastTransitionTime:
+ description: |-
+ lastTransitionTime is the last time the condition transitioned from one status to another.
+ This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: |-
+ message is a human readable message indicating details about the transition.
+ This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: |-
+ observedGeneration represents the .metadata.generation that the condition was set based upon.
+ For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: |-
+ reason contains a programmatic identifier indicating the reason for the condition's last transition.
+ Producers of specific condition types may define expected values and meanings for this field,
+ and whether the values are considered a guaranteed API.
+ The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ pattern: ^([A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?)?$
+ type: string
+ status:
+ description: status of the condition, one of True, False, Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: type of condition in CamelCase or in foo.example.com/CamelCase.
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - status
+ - type
+ type: object
+ type: array
+ info:
+ description: Info can contain any additional like reconciliation errors
+ type: string
+ ready:
+ description: Ready indicates if the resource is seen as completely
+ reconciled
+ type: boolean
+ type: object
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/config/crd/bases/formance.com_gateways.yaml b/config/crd/bases/formance.com_gateways.yaml
index aaf0f6acc..71265f185 100644
--- a/config/crd/bases/formance.com_gateways.yaml
+++ b/config/crd/bases/formance.com_gateways.yaml
@@ -184,6 +184,11 @@ spec:
description: Ready indicates if the resource is seen as completely
reconciled
type: boolean
+ syncGRPCAPIs:
+ description: Detected grpc apis. See [GatewayGRPCAPI](#gatewaygrpcapi)
+ items:
+ type: string
+ type: array
syncHTTPAPIs:
description: Detected http apis. See [GatewayHTTPAPI](#gatewayhttpapi)
items:
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
index 65e9b2c58..7d21bf0ce 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -5,6 +5,7 @@ resources:
- bases/formance.com_databases.yaml
- bases/formance.com_stacks.yaml
- bases/formance.com_brokertopics.yaml
+- bases/formance.com_gatewaygrpcapis.yaml
- bases/formance.com_gatewayhttpapis.yaml
- bases/formance.com_ledgers.yaml
- bases/formance.com_gateways.yaml
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 9e2bae12a..87f27b9d5 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -89,6 +89,7 @@ rules:
- brokers
- brokertopics
- databases
+ - gatewaygrpcapis
- gatewayhttpapis
- gateways
- ledgers
@@ -124,6 +125,7 @@ rules:
- brokers/finalizers
- brokertopics/finalizers
- databases/finalizers
+ - gatewaygrpcapis/finalizers
- gatewayhttpapis/finalizers
- gateways/finalizers
- ledgers/finalizers
@@ -153,6 +155,7 @@ rules:
- brokers/status
- brokertopics/status
- databases/status
+ - gatewaygrpcapis/status
- gatewayhttpapis/status
- gateways/status
- ledgers/status
diff --git a/deployment/operator/.gitignore b/deployment/operator/.gitignore
new file mode 100644
index 000000000..7dc315b66
--- /dev/null
+++ b/deployment/operator/.gitignore
@@ -0,0 +1,2 @@
+deployment/operator/bin/
+operator
diff --git a/deployment/operator/Pulumi.yaml b/deployment/operator/Pulumi.yaml
new file mode 100644
index 000000000..0b8aac981
--- /dev/null
+++ b/deployment/operator/Pulumi.yaml
@@ -0,0 +1,68 @@
+name: formance-operator
+runtime:
+ name: go
+ options:
+ buildTarget: ./bin/pulumi
+description: Formance operator deployment (CRDs, operator)
+x-plr-config:
+ k8s-context:
+ type: string
+ required: true
+ description: Kubernetes context to use
+ namespace:
+ type: string
+ description: Kubernetes namespace (defaults to stack name)
+ registry:
+ type: string
+ default: ghcr.io
+ description: Docker registry for building images
+ pull-registry:
+ type: string
+ description: Docker registry for pulling images (defaults to registry)
+ docker-builder-name:
+ type: string
+ description: Docker buildx builder name
+ imageTag:
+ type: string
+ description: Image tag (defaults to git version)
+ arch:
+ type: string
+ default: amd64
+ description: Target CPU architecture
+ registry-username:
+ type: string
+ secret: true
+ description: Docker registry username
+ registry-password:
+ type: string
+ secret: true
+ description: Docker registry password
+ image-pull-secrets:
+ type: array
+ description: "Image pull secrets [{name: string}]"
+ operator-region:
+ type: string
+ default: eu-west-1
+ description: Region passed to the operator
+ operator-env:
+ type: string
+ default: staging
+ description: Environment passed to the operator
+ operator-dev:
+ type: boolean
+ default: false
+ description: Enable operator dev mode
+ licence-token:
+ type: string
+ secret: true
+ description: Formance licence token
+ licence-issuer:
+ type: string
+ default: "https://license.formance.cloud/keys"
+ description: Formance licence issuer URL
+ node-selector:
+ type: object
+ description: "Node selector for the operator pod"
+ tolerations:
+ type: array
+ description: "Tolerations for the operator pod"
diff --git a/deployment/operator/go.mod b/deployment/operator/go.mod
new file mode 100644
index 000000000..550b43c4b
--- /dev/null
+++ b/deployment/operator/go.mod
@@ -0,0 +1,121 @@
+module github.com/formancehq/operator/v3/deployment/operator
+
+go 1.26.2
+
+require (
+ github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild v0.0.18
+ github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.31.1
+ github.com/pulumi/pulumi/sdk/v3 v3.243.0
+ gopkg.in/yaml.v3 v3.0.1
+)
+
+require (
+ dario.cat/mergo v1.0.1 // indirect
+ github.com/BurntSushi/toml v1.6.0 // indirect
+ github.com/Microsoft/go-winio v0.6.2 // indirect
+ github.com/ProtonMail/go-crypto v1.2.0 // indirect
+ github.com/agext/levenshtein v1.2.3 // indirect
+ github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
+ github.com/atotto/clipboard v0.1.4 // indirect
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+ github.com/blang/semver v3.5.1+incompatible // indirect
+ github.com/cenkalti/backoff/v5 v5.0.3 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/charmbracelet/bubbles v1.0.0 // indirect
+ github.com/charmbracelet/bubbletea v1.3.10 // indirect
+ github.com/charmbracelet/colorprofile v0.4.3 // indirect
+ github.com/charmbracelet/lipgloss v1.1.0 // indirect
+ github.com/charmbracelet/x/ansi v0.11.7 // indirect
+ github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
+ github.com/charmbracelet/x/term v0.2.2 // indirect
+ github.com/cheggaaa/pb v1.0.29 // indirect
+ github.com/clipperhouse/displaywidth v0.11.0 // indirect
+ github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
+ github.com/cloudflare/circl v1.6.3 // indirect
+ github.com/cyphar/filepath-securejoin v0.6.1 // indirect
+ github.com/djherbis/times v1.6.0 // indirect
+ github.com/emirpasic/gods v1.18.1 // indirect
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
+ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
+ github.com/go-git/go-billy/v5 v5.9.0 // indirect
+ github.com/go-git/go-git/v5 v5.19.1 // indirect
+ github.com/go-logr/logr v1.4.3 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang/glog v1.2.5 // indirect
+ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
+ github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
+ github.com/hashicorp/errwrap v1.1.0 // indirect
+ github.com/hashicorp/go-multierror v1.1.1 // indirect
+ github.com/hashicorp/go-version v1.9.0 // indirect
+ github.com/hashicorp/hcl/v2 v2.24.0 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/kevinburke/ssh_config v1.2.0 // indirect
+ github.com/klauspost/compress v1.18.0 // indirect
+ github.com/klauspost/cpuid/v2 v2.3.0 // indirect
+ github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
+ github.com/mattn/go-isatty v0.0.22 // indirect
+ github.com/mattn/go-localereader v0.0.1 // indirect
+ github.com/mattn/go-runewidth v0.0.23 // indirect
+ github.com/mitchellh/go-ps v1.0.0 // indirect
+ github.com/mitchellh/go-wordwrap v1.0.1 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
+ github.com/muesli/cancelreader v0.2.2 // indirect
+ github.com/muesli/termenv v0.16.0 // indirect
+ github.com/opentracing/basictracer-go v1.1.0 // indirect
+ github.com/opentracing/opentracing-go v1.2.0 // indirect
+ github.com/pgavlin/fx v0.1.6 // indirect
+ github.com/pgavlin/fx/v2 v2.0.12 // indirect
+ github.com/pjbgf/sha1cd v0.6.0 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/pkg/term v1.1.0 // indirect
+ github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 // indirect
+ github.com/pulumi/esc v0.24.0 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/rogpeppe/go-internal v1.14.1 // indirect
+ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect
+ github.com/sergi/go-diff v1.4.0 // indirect
+ github.com/skeema/knownhosts v1.3.1 // indirect
+ github.com/spf13/cast v1.5.0 // indirect
+ github.com/spf13/cobra v1.10.2 // indirect
+ github.com/spf13/pflag v1.0.10 // indirect
+ github.com/texttheater/golang-levenshtein v1.0.1 // indirect
+ github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
+ github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
+ github.com/xanzy/ssh-agent v0.3.3 // indirect
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+ github.com/zclconf/go-cty v1.17.0 // indirect
+ go.opentelemetry.io/auto/sdk v1.2.1 // indirect
+ go.opentelemetry.io/collector/featuregate v1.58.0 // indirect
+ go.opentelemetry.io/collector/pdata v1.58.0 // indirect
+ go.opentelemetry.io/otel v1.43.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect
+ go.opentelemetry.io/otel/metric v1.43.0 // indirect
+ go.opentelemetry.io/otel/sdk v1.43.0 // indirect
+ go.opentelemetry.io/otel/trace v1.43.0 // indirect
+ go.opentelemetry.io/proto/otlp v1.10.0 // indirect
+ go.uber.org/atomic v1.11.0 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ golang.org/x/crypto v0.51.0 // indirect
+ golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
+ golang.org/x/mod v0.35.0 // indirect
+ golang.org/x/net v0.54.0 // indirect
+ golang.org/x/sync v0.20.0 // indirect
+ golang.org/x/sys v0.44.0 // indirect
+ golang.org/x/term v0.43.0 // indirect
+ golang.org/x/text v0.37.0 // indirect
+ golang.org/x/tools v0.44.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94 // indirect
+ google.golang.org/grpc v1.81.1 // indirect
+ google.golang.org/protobuf v1.36.11 // indirect
+ gopkg.in/warnings.v0 v0.1.2 // indirect
+ lukechampine.com/frand v1.5.1 // indirect
+)
diff --git a/deployment/operator/go.sum b/deployment/operator/go.sum
new file mode 100644
index 000000000..e4ba939bd
--- /dev/null
+++ b/deployment/operator/go.sum
@@ -0,0 +1,362 @@
+dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
+dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
+github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM=
+github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
+github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs=
+github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
+github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
+github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
+github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
+github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
+github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
+github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
+github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
+github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
+github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
+github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
+github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
+github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
+github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
+github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
+github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
+github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
+github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
+github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
+github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
+github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
+github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
+github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo=
+github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30=
+github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
+github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
+github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
+github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
+github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
+github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
+github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
+github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
+github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
+github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
+github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
+github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
+github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
+github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
+github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
+github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
+github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
+github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
+github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
+github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
+github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
+github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
+github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I=
+github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
+github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU=
+github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
+github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
+github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
+github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
+github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
+github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
+github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
+github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
+github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
+github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
+github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
+github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
+github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
+github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
+github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
+github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
+github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
+github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
+github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
+github.com/opentracing/basictracer-go v1.1.0 h1:Oa1fTSBvAl8pa3U+IJYqrKm0NALwH9OsgwOqDv4xJW0=
+github.com/opentracing/basictracer-go v1.1.0/go.mod h1:V2HZueSJEp879yv285Aap1BS69fQMD+MNP1mRs6mBQc=
+github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
+github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
+github.com/pgavlin/fx v0.1.6 h1:r9jEg69DhNoCd3Xh0+5mIbdbS3PqWrVWujkY76MFRTU=
+github.com/pgavlin/fx v0.1.6/go.mod h1:KWZJ6fqBBSh8GxHYqwYCf3rYE7Gp2p0N8tJp8xv9u9M=
+github.com/pgavlin/fx/v2 v2.0.12 h1:SjjaJ68Dt8Z4zHwOpY/RPijd7lShs6xYupJbF9ra00M=
+github.com/pgavlin/fx/v2 v2.0.12/go.mod h1:M/nF/ooAOy+NUBooYYXl2REARzJ/giPJxfMs8fINfKc=
+github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
+github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk=
+github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 h1:vkHw5I/plNdTr435cARxCW6q9gc0S/Yxz7Mkd38pOb0=
+github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231/go.mod h1:murToZ2N9hNJzewjHBgfFdXhZKjY3z5cYC1VXk+lbFE=
+github.com/pulumi/esc v0.24.0 h1:sCtiB0qbyrlU1ZNzJn4dTLYiChl8xeCBFbHWl1YoXJg=
+github.com/pulumi/esc v0.24.0/go.mod h1:eCOOkcDJS6eooGwdE4/E0+pOsvUWG254+KBmPCFwJpA=
+github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild v0.0.18 h1:emkSEfjXfz7i2vNDi43WTqABhP9TY2mQnO2zdL683hw=
+github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild v0.0.18/go.mod h1:BriBqoV2I/58/AZy4/4oJfoiJYX7Nf/NxsAmGXDgvgo=
+github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.31.1 h1:Hg9RK9zqIU9kFbD5KeiON06gPP7cLgS68jvsgMBmPgw=
+github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.31.1/go.mod h1:BAWI9R3JEEGOp1JlXLPSZKwBGANSrPGUWKtMnS5w5qw=
+github.com/pulumi/pulumi/sdk/v3 v3.243.0 h1:pZaMx58nXrdh4XB0cgTlHnL3EMy3/JQwuin3aDuWyRM=
+github.com/pulumi/pulumi/sdk/v3 v3.243.0/go.mod h1:BPWWuYPXcPH5YbXGoyy9Rrfa+evrh6IdM51AjDhcDpM=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
+github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
+github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
+github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
+github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
+github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
+github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
+github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
+github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U=
+github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8=
+github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
+github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
+github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
+github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
+github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
+github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0=
+github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U=
+go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
+go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
+go.opentelemetry.io/collector/featuregate v1.58.0 h1:Kh6Dpgbxywv/Q3D6qPehaSxNCxvr/U/ki7CL4y3udCo=
+go.opentelemetry.io/collector/featuregate v1.58.0/go.mod h1:4ga1QBMPEejXXmpyJS8lmaRpknJ3Lb9Bvk6e420bUFU=
+go.opentelemetry.io/collector/internal/testutil v0.152.0 h1:8LGwekR7mLcUDhT1ofLmdnrHRFuUa3U7PBd95ZvJEjQ=
+go.opentelemetry.io/collector/internal/testutil v0.152.0/go.mod h1:Jkjs6rkqs973LqgZ0Fe3zrokQRKULYXPIf4HuqStiEE=
+go.opentelemetry.io/collector/pdata v1.58.0 h1:5Lxut3NxKp87066Pzt+3q7+JUuFI5B3teCyLZIF8wIs=
+go.opentelemetry.io/collector/pdata v1.58.0/go.mod h1:4vZtODINbC/JF3eGocnatdImzbRHseOywIcr+aULjCg=
+go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
+go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=
+go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
+go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
+go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
+go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
+go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
+go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
+go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
+go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
+go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
+go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
+go.opentelemetry.io/proto/slim/otlp v1.10.0 h1:iR97Vs/ZDR+y9TfuP9b1XBtdPWeC+OMslIBmhcLU7jM=
+go.opentelemetry.io/proto/slim/otlp v1.10.0/go.mod h1:lV9250stpjYLPNA5viFabIgP2QlUGRT1GdTgAf8SIUk=
+go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0 h1:RUF5rO0hAlgiJt1fzQVzcVs3vZVNHIcMLgOgG4rWNcQ=
+go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0/go.mod h1:I89cynRj8y+383o7tEQVg2SVA6SRgDVIouWPUVXjx0U=
+go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.3.0 h1:CQvJSldHRUN6Z8jsUeYv8J0lXRvygALXIzsmAeCcZE0=
+go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.3.0/go.mod h1:xSQ+mEfJe/GjK1LXEyVOoSI1N9JV9ZI923X5kup43W4=
+go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
+go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
+golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
+golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
+golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
+golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
+golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
+golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
+golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
+golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
+golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
+golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
+gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
+google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94 h1:DddG61lE5LkX6144z22i0gma9BMBs5aZ9B8lZLobxyw=
+google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:1dCETSCY2YKZNXQE3h4fun3TYwF5p8jejRKZgfWAgAY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94 h1:eZCjr/aAF8c5ccm5pb6T4EXgIei5MlAAPWPJk+5ArfY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
+google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
+google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
+google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+lukechampine.com/frand v1.5.1 h1:fg0eRtdmGFIxhP5zQJzM1lFDbD6CUfu/f+7WgAZd5/w=
+lukechampine.com/frand v1.5.1/go.mod h1:4VstaWc2plN4Mjr10chUD46RAVGWhpkZ5Nja8+Azp0Q=
+pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
+pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
diff --git a/deployment/operator/helpers.go b/deployment/operator/helpers.go
new file mode 100644
index 000000000..44effc7eb
--- /dev/null
+++ b/deployment/operator/helpers.go
@@ -0,0 +1,272 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild"
+ "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes"
+ "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
+ "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
+ "gopkg.in/yaml.v3"
+)
+
+func getBuildVersion(gitDir string) string {
+ cmd := exec.Command("git", "rev-parse", "--short", "HEAD")
+ cmd.Dir = gitDir
+ output, err := cmd.Output()
+
+ timestamp := time.Now().Format("20060102-150405")
+
+ if err != nil {
+ return timestamp
+ }
+
+ commit := strings.TrimSpace(string(output))
+
+ cmd = exec.Command("git", "status", "--porcelain")
+ cmd.Dir = gitDir
+ statusOutput, _ := cmd.Output()
+
+ if len(statusOutput) > 0 {
+ return fmt.Sprintf("%s-dirty-%s", commit, timestamp)
+ }
+
+ return fmt.Sprintf("%s-%s", commit, timestamp)
+}
+
+func getConfigBool(cfg *config.Config, key string, fallback bool) bool {
+ value := cfg.GetBool(key)
+ if value {
+ return true
+ }
+ if cfg.Get(key) == "false" {
+ return false
+ }
+ return fallback
+}
+
+func newK8sProvider(ctx *pulumi.Context, cfg *config.Config) (pulumi.ProviderResource, error) {
+ kubeContext := cfg.Require("k8s-context")
+
+ k8sProvider, err := kubernetes.NewProvider(ctx, "k8s", &kubernetes.ProviderArgs{
+ Context: pulumi.StringPtr(kubeContext),
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to create k8s provider: %w", err)
+ }
+
+ return k8sProvider, nil
+}
+
+type dockerConfig struct {
+ Registry string
+ PullRegistry string
+ BuilderName string
+ ImageTag string
+ Platforms []string
+ RegistryAuth dockerbuild.RegistryArray
+}
+
+var allPlatforms = []string{"linux-amd64", "linux-arm64"}
+
+func newDockerConfig(ctx *pulumi.Context, cfg *config.Config) *dockerConfig {
+ registry := cfg.Get("registry")
+ if registry == "" {
+ registry = "ghcr.io"
+ }
+ pullRegistry := cfg.Get("pull-registry")
+ if pullRegistry == "" {
+ pullRegistry = registry
+ }
+ builderName := cfg.Get("docker-builder-name")
+
+ buildVersion := getBuildVersion("../..")
+ imageTag := cfg.Get("imageTag")
+ if imageTag == "" {
+ imageTag = buildVersion
+ }
+
+ arch := cfg.Get("arch")
+ if arch == "" {
+ arch = "amd64"
+ }
+ platforms := make([]string, 0, len(allPlatforms))
+ for _, p := range allPlatforms {
+ if strings.HasSuffix(p, arch) {
+ platforms = append(platforms, p)
+ }
+ }
+ if len(platforms) == 0 {
+ platforms = []string{"linux-" + arch}
+ }
+
+ return &dockerConfig{
+ Registry: registry,
+ PullRegistry: pullRegistry,
+ BuilderName: builderName,
+ ImageTag: imageTag,
+ Platforms: platforms,
+ RegistryAuth: dockerbuild.RegistryArray{
+ dockerbuild.RegistryArgs{
+ Address: pulumi.String(registry),
+ Username: config.GetSecret(ctx, "registry-username"),
+ Password: config.GetSecret(ctx, "registry-password"),
+ },
+ },
+ }
+}
+
+type multiArchImage struct {
+ Index *dockerbuild.Index
+ Images []*dockerbuild.Image
+ Ref pulumi.StringOutput
+ Digest pulumi.StringOutput
+}
+
+func (m *multiArchImage) Resource() pulumi.Resource {
+ return m.Index
+}
+
+func (dc *dockerConfig) buildImage(
+ ctx *pulumi.Context,
+ name string,
+ contextPath string,
+ dockerfilePath string,
+) (*multiArchImage, error) {
+ var sources pulumi.StringArray
+ var images []*dockerbuild.Image
+
+ for _, platform := range dc.Platforms {
+ img, err := dockerbuild.NewImage(ctx, fmt.Sprintf("%s-%s", name, platform), &dockerbuild.ImageArgs{
+ Context: dockerbuild.BuildContextArgs{
+ Location: pulumi.String(contextPath),
+ },
+ Builder: dockerbuild.BuilderConfigArgs{
+ Name: pulumi.String(dc.BuilderName),
+ },
+ CacheFrom: dockerbuild.CacheFromArray{
+ dockerbuild.CacheFromArgs{
+ Registry: dockerbuild.CacheFromRegistryArgs{
+ Ref: pulumi.Sprintf("%s/%s:buildcache-%s", dc.Registry, name, platform),
+ },
+ },
+ },
+ CacheTo: dockerbuild.CacheToArray{
+ dockerbuild.CacheToArgs{
+ Registry: dockerbuild.CacheToRegistryArgs{
+ Ref: pulumi.Sprintf("%s/%s:buildcache-%s,mode=max", dc.Registry, name, platform),
+ },
+ },
+ },
+ Dockerfile: dockerbuild.DockerfileArgs{
+ Location: pulumi.String(dockerfilePath),
+ },
+ Platforms: dockerbuild.PlatformArray{
+ dockerbuild.Platform(strings.ReplaceAll(platform, "-", "/")),
+ },
+ Push: pulumi.Bool(true),
+ Registries: dc.RegistryAuth,
+ Tags: pulumi.StringArray{
+ pulumi.Sprintf("%s/%s:%s-%s", dc.Registry, name, dc.ImageTag, platform),
+ },
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to build %s for %s: %w", name, platform, err)
+ }
+ sources = append(sources, img.Ref)
+ images = append(images, img)
+ }
+
+ idx, err := dockerbuild.NewIndex(ctx, name, &dockerbuild.IndexArgs{
+ Sources: sources,
+ Tag: pulumi.Sprintf("%s/%s:%s", dc.Registry, name, dc.ImageTag),
+ Push: pulumi.Bool(true),
+ Registry: dockerbuild.RegistryArgs{
+ Address: pulumi.String(dc.Registry),
+ Username: dc.RegistryAuth[0].(dockerbuild.RegistryArgs).Username,
+ Password: dc.RegistryAuth[0].(dockerbuild.RegistryArgs).Password,
+ },
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to create index for %s: %w", name, err)
+ }
+
+ digest := idx.Ref.ApplyT(func(ref string) string {
+ if i := strings.Index(ref, "@"); i >= 0 {
+ return ref[i+1:]
+ }
+ return ref
+ }).(pulumi.StringOutput)
+
+ return &multiArchImage{
+ Index: idx,
+ Images: images,
+ Ref: idx.Ref,
+ Digest: digest,
+ }, nil
+}
+
+func getConfigMap(cfg *config.Config, key string) pulumi.Map {
+ var obj map[string]any
+ if err := cfg.GetObject(key, &obj); err != nil || obj == nil {
+ return pulumi.Map{}
+ }
+ return pulumi.ToMap(obj)
+}
+
+func getConfigArray(cfg *config.Config, key string) pulumi.Array {
+ var arr []map[string]any
+ if err := cfg.GetObject(key, &arr); err != nil || arr == nil {
+ return pulumi.Array{}
+ }
+ result := make(pulumi.Array, len(arr))
+ for i, v := range arr {
+ result[i] = pulumi.ToMap(v)
+ }
+ return result
+}
+
+func getImagePullSecrets(cfg *config.Config) pulumi.Array {
+ var secrets []map[string]any
+ if err := cfg.GetObject("image-pull-secrets", &secrets); err != nil || len(secrets) == 0 {
+ return pulumi.Array{}
+ }
+ var result pulumi.Array
+ for _, s := range secrets {
+ if name, ok := s["name"].(string); ok && name != "" {
+ result = append(result, pulumi.Map{
+ "name": pulumi.String(name),
+ })
+ }
+ }
+ return result
+}
+
+func getConfigObject(cfg *config.Config, key string, basePath string) (map[string]any, error) {
+ var configObj map[string]any
+ if err := cfg.GetObject(key, &configObj); err != nil {
+ return nil, fmt.Errorf("failed to get config object %s: %w", key, err)
+ }
+
+ if filePath, ok := configObj["file"].(string); ok {
+ fullPath := filepath.Join(basePath, filePath)
+ data, err := os.ReadFile(fullPath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read values file %s: %w", fullPath, err)
+ }
+
+ var result map[string]any
+ if err := yaml.Unmarshal(data, &result); err != nil {
+ return nil, fmt.Errorf("failed to parse YAML file %s: %w", fullPath, err)
+ }
+
+ return result, nil
+ }
+
+ return configObj, nil
+}
diff --git a/deployment/operator/main.go b/deployment/operator/main.go
new file mode 100644
index 000000000..0c8ddb1c0
--- /dev/null
+++ b/deployment/operator/main.go
@@ -0,0 +1,128 @@
+package main
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+
+ k8syaml "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/yaml"
+ "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v3"
+ "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
+ "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
+)
+
+func main() {
+ pulumi.Run(func(ctx *pulumi.Context) error {
+ cfg := config.New(ctx, "")
+
+ k8sProvider, err := newK8sProvider(ctx, cfg)
+ if err != nil {
+ return err
+ }
+
+ namespace := cfg.Get("namespace")
+ if namespace == "" {
+ namespace = "formance-system"
+ }
+
+ dc := newDockerConfig(ctx, cfg)
+
+ // Build operator image
+ operatorImage, err := dc.buildImage(ctx, "formancehq/operator", "../..", "../../Dockerfile")
+ if err != nil {
+ return fmt.Errorf("failed to build operator image: %w", err)
+ }
+
+ // Apply CRDs
+ 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)
+ }
+
+ // Operator configuration
+ region := cfg.Get("operator-region")
+ if region == "" {
+ region = "eu-west-1"
+ }
+ env := cfg.Get("operator-env")
+ if env == "" {
+ env = "staging"
+ }
+
+ // Licence configuration
+ licenceValues := pulumi.Map{}
+ 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),
+ }
+ } else {
+ licenceValues = pulumi.Map{
+ "createSecret": pulumi.Bool(false),
+ }
+ }
+
+ // Deploy operator via Helm
+ operatorChartPath := filepath.Join("..", "..", "helm", "operator")
+
+ operatorRelease, err := helm.NewRelease(ctx, "formance-operator", &helm.ReleaseArgs{
+ Name: pulumi.String("formance-operator"),
+ Chart: pulumi.String(operatorChartPath),
+ Namespace: pulumi.String(namespace),
+ CreateNamespace: pulumi.Bool(true),
+ Values: pulumi.Map{
+ "operator-crds": pulumi.Map{
+ "create": pulumi.Bool(false),
+ },
+ "image": pulumi.Map{
+ "repository": pulumi.Sprintf("%s/formancehq/operator", dc.PullRegistry),
+ "tag": pulumi.Sprintf("latest@%s", operatorImage.Digest),
+ },
+ "imagePullSecrets": getImagePullSecrets(cfg),
+ "global": pulumi.Map{
+ "licence": licenceValues,
+ },
+ "operator": pulumi.Map{
+ "region": pulumi.String(region),
+ "env": pulumi.String(env),
+ "dev": pulumi.Bool(getConfigBool(cfg, "operator-dev", false)),
+ "enableLeaderElection": pulumi.Bool(true),
+ },
+ "nodeSelector": getConfigMap(cfg, "node-selector"),
+ "tolerations": getConfigArray(cfg, "tolerations"),
+ },
+ ForceUpdate: pulumi.Bool(true),
+ },
+ pulumi.DependsOn(append([]pulumi.Resource{operatorImage.Resource()}, crds...)),
+ pulumi.Provider(k8sProvider),
+ )
+ if err != nil {
+ return fmt.Errorf("failed to deploy operator: %w", err)
+ }
+
+ // Exports
+ ctx.Export("namespace", pulumi.String(namespace))
+ ctx.Export("operatorImage", pulumi.Sprintf("%s/formancehq/operator:latest@%s", dc.PullRegistry, operatorImage.Digest))
+ ctx.Export("operatorRelease", operatorRelease.Name)
+
+ return nil
+ })
+}
diff --git a/docs/09-Configuration reference/02-Custom Resource Definitions.md b/docs/09-Configuration reference/02-Custom Resource Definitions.md
index 3d03b7035..85134da07 100644
--- a/docs/09-Configuration reference/02-Custom Resource Definitions.md
+++ b/docs/09-Configuration reference/02-Custom Resource Definitions.md
@@ -39,6 +39,7 @@ Other resources :
- [BrokerConsumer](#brokerconsumer)
- [BrokerTopic](#brokertopic)
- [Database](#database)
+- [GatewayGRPCAPI](#gatewaygrpcapi)
- [GatewayHTTPAPI](#gatewayhttpapi)
- [ResourceReference](#resourcereference)
- [Versions](#versions)
@@ -561,6 +562,7 @@ Gateway is the Schema for the gateways API
| `ready` _boolean_ | Ready indicates if the resource is seen as completely reconciled | | |
| `info` _string_ | Info can contain any additional like reconciliation errors | | |
| `syncHTTPAPIs` _string array_ | Detected http apis. See [GatewayHTTPAPI](#gatewayhttpapi) | | |
+| `syncGRPCAPIs` _string array_ | Detected grpc apis. See [GatewayGRPCAPI](#gatewaygrpcapi) | | |
#### Ledger
@@ -2089,6 +2091,94 @@ It will be recreated with correct uri.
| `outOfSync` _boolean_ | OutOfSync indicates than a settings changed the uri of the postgres server
The Database object need to be removed to be recreated | | |
+#### GatewayGRPCAPI
+
+
+
+GatewayGRPCAPI is the Schema for the GRPCAPIs API
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `apiVersion` _string_ | `formance.com/v1beta1` | | |
+| `kind` _string_ | `GatewayGRPCAPI` | | |
+| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
+| `spec` _[GatewayGRPCAPISpec](#gatewaygrpcapispec)_ | | | |
+| `status` _[GatewayGRPCAPIStatus](#gatewaygrpcapistatus)_ | | | |
+
+
+
+##### GatewayGRPCAPISpec
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `stack` _string_ | Stack indicates the stack on which the module is installed | | |
+| `name` _string_ | Name indicates the module name (e.g. "ledger") | | |
+| `grpcServices` _string array_ | GRPCServices is the list of fully-qualified gRPC service names
exposed by this module (e.g. "formance.ledger.v1.LedgerService") | | |
+| `port` _integer_ | Port is the gRPC port on the backend service | 8081 | |
+
+
+
+
+
+##### GatewayGRPCAPIStatus
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+| 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_ | | | |
+
+
#### GatewayHTTPAPI
diff --git a/docs/09-Configuration reference/settings.catalog.json b/docs/09-Configuration reference/settings.catalog.json
index a4316ca86..1965cbfc3 100644
--- a/docs/09-Configuration reference/settings.catalog.json
+++ b/docs/09-Configuration reference/settings.catalog.json
@@ -195,35 +195,35 @@
"key": "gateway.caddyfile.grace-period",
"valueType": "string",
"sources": [
- "internal/resources/gateways/configuration.go:41"
+ "internal/resources/gateways/configuration.go:42"
]
},
{
"key": "gateway.caddyfile.shutdown-delay",
"valueType": "string",
"sources": [
- "internal/resources/gateways/configuration.go:33"
+ "internal/resources/gateways/configuration.go:34"
]
},
{
"key": "gateway.caddyfile.trusted-proxies",
"valueType": "string[]",
"sources": [
- "internal/resources/gateways/configuration.go:17"
+ "internal/resources/gateways/configuration.go:18"
]
},
{
"key": "gateway.caddyfile.trusted-proxies-strict",
"valueType": "bool",
"sources": [
- "internal/resources/gateways/configuration.go:25"
+ "internal/resources/gateways/configuration.go:26"
]
},
{
"key": "gateway.config.idle-timeout",
"valueType": "string",
"sources": [
- "internal/resources/gateways/configuration.go:49"
+ "internal/resources/gateways/configuration.go:50"
]
},
{
diff --git a/helm/crds/templates/crds/apiextensions.k8s.io_v1_customresourcedefinition_gatewaygrpcapis.formance.com.yaml b/helm/crds/templates/crds/apiextensions.k8s.io_v1_customresourcedefinition_gatewaygrpcapis.formance.com.yaml
new file mode 100644
index 000000000..43e2e0be0
--- /dev/null
+++ b/helm/crds/templates/crds/apiextensions.k8s.io_v1_customresourcedefinition_gatewaygrpcapis.formance.com.yaml
@@ -0,0 +1,143 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.18.0
+ helm.sh/resource-policy: keep
+ {{- with .Values.annotations }}
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+ name: gatewaygrpcapis.formance.com
+spec:
+ group: formance.com
+ names:
+ kind: GatewayGRPCAPI
+ listKind: GatewayGRPCAPIList
+ plural: gatewaygrpcapis
+ singular: gatewaygrpcapi
+ scope: Cluster
+ versions:
+ - additionalPrinterColumns:
+ - description: Stack
+ jsonPath: .spec.stack
+ name: Stack
+ type: string
+ - description: Ready
+ jsonPath: .status.ready
+ name: Ready
+ type: string
+ name: v1beta1
+ schema:
+ openAPIV3Schema:
+ description: GatewayGRPCAPI is the Schema for the GRPCAPIs API
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ properties:
+ grpcServices:
+ description: |-
+ GRPCServices is the list of fully-qualified gRPC service names
+ exposed by this module (e.g. "formance.ledger.v1.LedgerService")
+ items:
+ type: string
+ type: array
+ name:
+ description: Name indicates the module name (e.g. "ledger")
+ type: string
+ port:
+ default: 8081
+ description: Port is the gRPC port on the backend service
+ format: int32
+ type: integer
+ stack:
+ description: Stack indicates the stack on which the module is installed
+ type: string
+ required:
+ - grpcServices
+ - name
+ type: object
+ status:
+ properties:
+ conditions:
+ items:
+ description: Condition contains details for one aspect of the current
+ state of this API Resource.
+ properties:
+ lastTransitionTime:
+ description: |-
+ lastTransitionTime is the last time the condition transitioned from one status to another.
+ This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: |-
+ message is a human readable message indicating details about the transition.
+ This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: |-
+ observedGeneration represents the .metadata.generation that the condition was set based upon.
+ For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: |-
+ reason contains a programmatic identifier indicating the reason for the condition's last transition.
+ Producers of specific condition types may define expected values and meanings for this field,
+ and whether the values are considered a guaranteed API.
+ The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ pattern: ^([A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?)?$
+ type: string
+ status:
+ description: status of the condition, one of True, False, Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: type of condition in CamelCase or in foo.example.com/CamelCase.
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - status
+ - type
+ type: object
+ type: array
+ info:
+ description: Info can contain any additional like reconciliation errors
+ type: string
+ ready:
+ description: Ready indicates if the resource is seen as completely
+ reconciled
+ type: boolean
+ type: object
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/helm/crds/templates/crds/apiextensions.k8s.io_v1_customresourcedefinition_gateways.formance.com.yaml b/helm/crds/templates/crds/apiextensions.k8s.io_v1_customresourcedefinition_gateways.formance.com.yaml
index 91c103c20..525405009 100644
--- a/helm/crds/templates/crds/apiextensions.k8s.io_v1_customresourcedefinition_gateways.formance.com.yaml
+++ b/helm/crds/templates/crds/apiextensions.k8s.io_v1_customresourcedefinition_gateways.formance.com.yaml
@@ -187,6 +187,11 @@ spec:
description: Ready indicates if the resource is seen as completely
reconciled
type: boolean
+ syncGRPCAPIs:
+ description: Detected grpc apis. See [GatewayGRPCAPI](#gatewaygrpcapi)
+ items:
+ type: string
+ type: array
syncHTTPAPIs:
description: Detected http apis. See [GatewayHTTPAPI](#gatewayhttpapi)
items:
diff --git a/helm/operator/templates/gen/rbac.authorization.k8s.io_v1_clusterrole_formance-manager-role.yaml b/helm/operator/templates/gen/rbac.authorization.k8s.io_v1_clusterrole_formance-manager-role.yaml
index 24e51fa55..09a9934c5 100644
--- a/helm/operator/templates/gen/rbac.authorization.k8s.io_v1_clusterrole_formance-manager-role.yaml
+++ b/helm/operator/templates/gen/rbac.authorization.k8s.io_v1_clusterrole_formance-manager-role.yaml
@@ -88,6 +88,7 @@ rules:
- brokers
- brokertopics
- databases
+ - gatewaygrpcapis
- gatewayhttpapis
- gateways
- ledgers
@@ -123,6 +124,7 @@ rules:
- brokers/finalizers
- brokertopics/finalizers
- databases/finalizers
+ - gatewaygrpcapis/finalizers
- gatewayhttpapis/finalizers
- gateways/finalizers
- ledgers/finalizers
@@ -152,6 +154,7 @@ rules:
- brokers/status
- brokertopics/status
- databases/status
+ - gatewaygrpcapis/status
- gatewayhttpapis/status
- gateways/status
- ledgers/status
diff --git a/internal/resources/all.go b/internal/resources/all.go
index 619fde7af..d05c5e5fe 100644
--- a/internal/resources/all.go
+++ b/internal/resources/all.go
@@ -8,6 +8,7 @@ import (
_ "github.com/formancehq/operator/v3/internal/resources/brokers"
_ "github.com/formancehq/operator/v3/internal/resources/brokertopics"
_ "github.com/formancehq/operator/v3/internal/resources/databases"
+ _ "github.com/formancehq/operator/v3/internal/resources/gatewaygrpcapis"
_ "github.com/formancehq/operator/v3/internal/resources/gatewayhttpapis"
_ "github.com/formancehq/operator/v3/internal/resources/gateways"
_ "github.com/formancehq/operator/v3/internal/resources/ledgers"
diff --git a/internal/resources/gatewaygrpcapis/create.go b/internal/resources/gatewaygrpcapis/create.go
new file mode 100644
index 000000000..0d2b2bde0
--- /dev/null
+++ b/internal/resources/gatewaygrpcapis/create.go
@@ -0,0 +1,45 @@
+package gatewaygrpcapis
+
+import (
+ "k8s.io/apimachinery/pkg/types"
+
+ v1beta1 "github.com/formancehq/operator/v3/api/formance.com/v1beta1"
+ "github.com/formancehq/operator/v3/internal/core"
+)
+
+type option func(spec *v1beta1.GatewayGRPCAPI)
+
+func Create(ctx core.Context, owner v1beta1.Module, options ...option) error {
+ objectName := core.LowerCaseKind(ctx, owner)
+ _, _, err := core.CreateOrUpdate[*v1beta1.GatewayGRPCAPI](ctx, types.NamespacedName{
+ Name: core.GetObjectName(owner.GetStack(), core.LowerCaseKind(ctx, owner)),
+ },
+ func(t *v1beta1.GatewayGRPCAPI) error {
+ t.Spec = v1beta1.GatewayGRPCAPISpec{
+ StackDependency: v1beta1.StackDependency{
+ Stack: owner.GetStack(),
+ },
+ Name: objectName,
+ }
+ for _, option := range options {
+ option(t)
+ }
+
+ return nil
+ },
+ core.WithController[*v1beta1.GatewayGRPCAPI](ctx.GetScheme(), owner),
+ )
+ return err
+}
+
+func WithGRPCServices(services ...string) func(grpcapi *v1beta1.GatewayGRPCAPI) {
+ return func(grpcapi *v1beta1.GatewayGRPCAPI) {
+ grpcapi.Spec.GRPCServices = services
+ }
+}
+
+func WithPort(port int32) func(grpcapi *v1beta1.GatewayGRPCAPI) {
+ return func(grpcapi *v1beta1.GatewayGRPCAPI) {
+ grpcapi.Spec.Port = port
+ }
+}
diff --git a/internal/resources/gatewaygrpcapis/init.go b/internal/resources/gatewaygrpcapis/init.go
new file mode 100644
index 000000000..1f577a0a1
--- /dev/null
+++ b/internal/resources/gatewaygrpcapis/init.go
@@ -0,0 +1,54 @@
+/*
+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 gatewaygrpcapis
+
+import (
+ corev1 "k8s.io/api/core/v1"
+
+ v1beta1 "github.com/formancehq/operator/v3/api/formance.com/v1beta1"
+ . "github.com/formancehq/operator/v3/internal/core"
+ "github.com/formancehq/operator/v3/internal/resources/services"
+)
+
+//+kubebuilder:rbac:groups=formance.com,resources=gatewaygrpcapis,verbs=get;list;watch;create;update;patch;delete
+//+kubebuilder:rbac:groups=formance.com,resources=gatewaygrpcapis/status,verbs=get;update;patch
+//+kubebuilder:rbac:groups=formance.com,resources=gatewaygrpcapis/finalizers,verbs=update
+
+func Reconcile(ctx Context, _ *v1beta1.Stack, grpcAPI *v1beta1.GatewayGRPCAPI) error {
+ _, err := services.Create(ctx, grpcAPI, grpcAPI.Spec.Name+"-grpc",
+ services.WithConfig(services.PortConfig{
+ ServiceName: grpcAPI.Spec.Name,
+ PortName: "grpc",
+ Port: grpcAPI.Spec.Port,
+ TargetPort: "grpc",
+ }),
+ )
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func init() {
+ Init(
+ WithStackDependencyReconciler(Reconcile,
+ WithOwn[*v1beta1.GatewayGRPCAPI](&corev1.Service{}),
+ WithWatchSettings[*v1beta1.GatewayGRPCAPI](),
+ ),
+ )
+}
diff --git a/internal/resources/gateways/Caddyfile.gotpl b/internal/resources/gateways/Caddyfile.gotpl
index 32913fc25..b074fd2b6 100644
--- a/internal/resources/gateways/Caddyfile.gotpl
+++ b/internal/resources/gateways/Caddyfile.gotpl
@@ -58,6 +58,9 @@
{{- end }}
servers {
+ {{- if .GRPCServices }}
+ protocols h1 h2c
+ {{- end }}
{{- if and .TrustedProxies (gt (len .TrustedProxies) 0) }}
trusted_proxies {{ .TrustedProxies }}
{{- end }}
@@ -143,6 +146,18 @@
}
}
+ {{- range $i, $service := .GRPCServices }}
+ {{- range $j, $svcName := $service.GRPCServices }}
+ handle /{{ $svcName }}/* {
+ reverse_proxy {{ $service.Name }}-grpc:{{ $service.Port }} {
+ transport http {
+ versions h2c 2
+ }
+ }
+ }
+ {{- end }}
+ {{- end }}
+
# Respond 404 if service does not exists
handle /api/* {
respond "Not Found" 404
diff --git a/internal/resources/gateways/caddyfile.go b/internal/resources/gateways/caddyfile.go
index 629abb747..3ff8652ad 100644
--- a/internal/resources/gateways/caddyfile.go
+++ b/internal/resources/gateways/caddyfile.go
@@ -14,12 +14,16 @@ import (
type CaddyOptions func(data map[string]any) error
func CreateCaddyfile(ctx core.Context, stack *v1beta1.Stack,
- gateway *v1beta1.Gateway, httpAPIs []*v1beta1.GatewayHTTPAPI, broker *v1beta1.Broker, options ...CaddyOptions) (string, error) {
+ gateway *v1beta1.Gateway, httpAPIs []*v1beta1.GatewayHTTPAPI,
+ grpcAPIs []*v1beta1.GatewayGRPCAPI, broker *v1beta1.Broker, options ...CaddyOptions) (string, error) {
data := map[string]any{
"Services": collectionutils.Map(httpAPIs, func(from *v1beta1.GatewayHTTPAPI) v1beta1.GatewayHTTPAPISpec {
return from.Spec
}),
+ "GRPCServices": collectionutils.Map(grpcAPIs, func(from *v1beta1.GatewayGRPCAPI) v1beta1.GatewayGRPCAPISpec {
+ return from.Spec
+ }),
"Platform": ctx.GetPlatform(),
"Debug": stack.Spec.Debug,
"Port": 8080,
diff --git a/internal/resources/gateways/configuration.go b/internal/resources/gateways/configuration.go
index 1902ad650..62b39323a 100644
--- a/internal/resources/gateways/configuration.go
+++ b/internal/resources/gateways/configuration.go
@@ -10,7 +10,8 @@ import (
)
func createConfigMap(ctx core.Context, stack *v1beta1.Stack,
- gateway *v1beta1.Gateway, httpAPIs []*v1beta1.GatewayHTTPAPI, broker *v1beta1.Broker) (*v1.ConfigMap, error) {
+ gateway *v1beta1.Gateway, httpAPIs []*v1beta1.GatewayHTTPAPI,
+ grpcAPIs []*v1beta1.GatewayGRPCAPI, broker *v1beta1.Broker) (*v1.ConfigMap, error) {
options := []CaddyOptions{}
@@ -54,7 +55,7 @@ func createConfigMap(ctx core.Context, stack *v1beta1.Stack,
options = append(options, withIdleTimeout(*idleTimeout))
}
- caddyfile, err := CreateCaddyfile(ctx, stack, gateway, httpAPIs, broker, options...)
+ caddyfile, err := CreateCaddyfile(ctx, stack, gateway, httpAPIs, grpcAPIs, broker, options...)
if err != nil {
return nil, err
}
diff --git a/internal/resources/gateways/init.go b/internal/resources/gateways/init.go
index 347964b8c..d0cd250c2 100644
--- a/internal/resources/gateways/init.go
+++ b/internal/resources/gateways/init.go
@@ -60,6 +60,20 @@ func Reconcile(ctx Context, stack *v1beta1.Stack, gateway *v1beta1.Gateway, vers
return from.Spec.Name
})
+ grpcAPIs := make([]*v1beta1.GatewayGRPCAPI, 0)
+ err = GetAllStackDependencies(ctx, gateway.Spec.Stack, &grpcAPIs)
+ if err != nil {
+ return err
+ }
+
+ sort.Slice(grpcAPIs, func(i, j int) bool {
+ return grpcAPIs[i].Spec.Name < grpcAPIs[j].Spec.Name
+ })
+
+ gateway.Status.SyncGRPCAPIs = Map(grpcAPIs, func(from *v1beta1.GatewayGRPCAPI) string {
+ return from.Spec.Name
+ })
+
var broker *v1beta1.Broker
if t, err := brokertopics.Find(ctx, stack, "gateway"); err != nil {
return err
@@ -78,7 +92,7 @@ func Reconcile(ctx Context, stack *v1beta1.Stack, gateway *v1beta1.Gateway, vers
}
}
- configMap, err := createConfigMap(ctx, stack, gateway, httpAPIs, broker)
+ configMap, err := createConfigMap(ctx, stack, gateway, httpAPIs, grpcAPIs, broker)
if err != nil {
return err
}
@@ -160,6 +174,7 @@ func init() {
}),
WithWatchSettings[*v1beta1.Gateway](),
WithWatchDependency[*v1beta1.Gateway](&v1beta1.GatewayHTTPAPI{}),
+ WithWatchDependency[*v1beta1.Gateway](&v1beta1.GatewayGRPCAPI{}),
WithWatchDependency[*v1beta1.Gateway](&v1beta1.Auth{}),
brokertopics.Watch[*v1beta1.Gateway]("gateway"),
),
diff --git a/internal/tests/gateway_controller_test.go b/internal/tests/gateway_controller_test.go
index 19eb9eae6..a7228b2fa 100644
--- a/internal/tests/gateway_controller_test.go
+++ b/internal/tests/gateway_controller_test.go
@@ -267,6 +267,38 @@ var _ = Describe("GatewayController", func() {
}).Should(MatchGoldenFile("gateway-controller", "configmap-with-ledger-and-another-service.yaml"))
})
})
+ Context("Then adding a GRPCService", func() {
+ var grpcAPI *v1beta1.GatewayGRPCAPI
+ BeforeEach(func() {
+ grpcAPI = &v1beta1.GatewayGRPCAPI{
+ ObjectMeta: RandObjectMeta(),
+ Spec: v1beta1.GatewayGRPCAPISpec{
+ StackDependency: v1beta1.StackDependency{
+ Stack: stack.Name,
+ },
+ Name: "mymodule",
+ GRPCServices: []string{"formance.mymodule.v1.MyService"},
+ Port: 8081,
+ },
+ }
+ Expect(Create(grpcAPI)).To(Succeed())
+ })
+ AfterEach(func() {
+ Expect(Delete(grpcAPI)).To(Succeed())
+ })
+ It("Should update the gateway status and Caddyfile with gRPC service", func() {
+ Eventually(func(g Gomega) []string {
+ g.Expect(LoadResource("", gateway.Name, gateway))
+ return gateway.Status.SyncGRPCAPIs
+ }).Should(ContainElements(grpcAPI.Spec.Name))
+
+ Eventually(func(g Gomega) string {
+ cm := &corev1.ConfigMap{}
+ g.Expect(LoadResource(stack.Name, "gateway", cm)).To(Succeed())
+ return cm.Data["Caddyfile"]
+ }).Should(MatchGoldenFile("gateway-controller", "configmap-with-ledger-and-grpc.yaml"))
+ })
+ })
Context("With a consumer on gateway", func() {
var (
brokerNatsDSNSettings *v1beta1.Settings
diff --git a/internal/tests/gatewaygrpcapi_controller_test.go b/internal/tests/gatewaygrpcapi_controller_test.go
new file mode 100644
index 000000000..a4ced33e0
--- /dev/null
+++ b/internal/tests/gatewaygrpcapi_controller_test.go
@@ -0,0 +1,78 @@
+package tests_test
+
+import (
+ "github.com/google/uuid"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ corev1 "k8s.io/api/core/v1"
+
+ v1beta1 "github.com/formancehq/operator/v3/api/formance.com/v1beta1"
+ "github.com/formancehq/operator/v3/internal/resources/settings"
+ . "github.com/formancehq/operator/v3/internal/tests/internal"
+)
+
+var _ = Describe("GatewayGRPCAPI", func() {
+ Context("When creating a GatewayGRPCAPI", func() {
+ var (
+ stack *v1beta1.Stack
+ grpcAPI *v1beta1.GatewayGRPCAPI
+ )
+ BeforeEach(func() {
+ stack = &v1beta1.Stack{
+ ObjectMeta: RandObjectMeta(),
+ Spec: v1beta1.StackSpec{Version: "v99.0.0"},
+ }
+ grpcAPI = &v1beta1.GatewayGRPCAPI{
+ ObjectMeta: RandObjectMeta(),
+ Spec: v1beta1.GatewayGRPCAPISpec{
+ StackDependency: v1beta1.StackDependency{
+ Stack: stack.Name,
+ },
+ Name: "mymodule",
+ GRPCServices: []string{"formance.mymodule.v1.MyService"},
+ Port: 8081,
+ },
+ }
+ })
+ JustBeforeEach(func() {
+ Expect(Create(stack)).To(BeNil())
+ Expect(Create(grpcAPI)).To(Succeed())
+ })
+ AfterEach(func() {
+ Expect(Delete(grpcAPI)).To(Succeed())
+ Expect(Delete(stack)).To(BeNil())
+ })
+ It("Should create a k8s service with -grpc suffix", func() {
+ service := &corev1.Service{}
+ Eventually(func() error {
+ return LoadResource(stack.Name, "mymodule-grpc", service)
+ }).Should(BeNil())
+ Expect(service).To(BeControlledBy(grpcAPI))
+ Expect(service.Spec.Selector).To(Equal(map[string]string{
+ "app.kubernetes.io/name": grpcAPI.Spec.Name,
+ }))
+ Expect(service.Spec.Ports).To(HaveLen(1))
+ Expect(service.Spec.Ports[0].Name).To(Equal("grpc"))
+ Expect(service.Spec.Ports[0].Port).To(Equal(int32(8081)))
+ })
+ Context("With user defined annotations", func() {
+ var (
+ annotationsSettings *v1beta1.Settings
+ )
+ JustBeforeEach(func() {
+ annotationsSettings = settings.New(uuid.NewString(), "services.*.annotations", "foo=bar", stack.Name)
+ Expect(Create(annotationsSettings)).To(Succeed())
+ })
+ JustAfterEach(func() {
+ Expect(Delete(annotationsSettings)).To(Succeed())
+ })
+ It("should add annotations to the service", func() {
+ Eventually(func(g Gomega) map[string]string {
+ service := &corev1.Service{}
+ g.Expect(LoadResource(stack.Name, "mymodule-grpc", service)).To(Succeed())
+ return service.Annotations
+ }).Should(HaveKeyWithValue("foo", "bar"))
+ })
+ })
+ })
+})
diff --git a/internal/tests/testdata/resources/gateway-controller/configmap-with-ledger-and-grpc.yaml b/internal/tests/testdata/resources/gateway-controller/configmap-with-ledger-and-grpc.yaml
new file mode 100644
index 000000000..507cdb912
--- /dev/null
+++ b/internal/tests/testdata/resources/gateway-controller/configmap-with-ledger-and-grpc.yaml
@@ -0,0 +1,64 @@
+(cors) {
+ header {
+ defer
+ Access-Control-Allow-Methods "GET,OPTIONS,PUT,POST,DELETE,HEAD,PATCH"
+ Access-Control-Allow-Headers content-type
+ Access-Control-Max-Age 100
+ Access-Control-Allow-Origin *
+ }
+}
+
+{
+ # Global metrics endpoint (moved from servers block - deprecated location)
+ metrics
+
+ servers {
+ protocols h1 h2c
+ }
+
+ admin :3080
+
+ # Many directives manipulate the HTTP handler chain and the order in which
+ # those directives are evaluated matters. So the jwtauth directive must be
+ # ordered.
+ # c.f. https://caddyserver.com/docs/caddyfile/directives#directive-order
+ order versions after metrics
+}
+
+:8080 {
+
+ log {
+ output stdout
+ }
+ handle /api/ledger* {
+ uri strip_prefix /api/ledger
+ import cors
+ reverse_proxy ledger:8080 {
+ header_up Host {upstream_hostport}
+ }
+ }
+
+ handle /versions {
+ versions {
+ region "us-west-1"
+ env "staging"
+ endpoints {
+ ledger {
+ http://ledger:8080/_info http://ledger:8080/
+ }
+ }
+ }
+ }
+ handle /formance.mymodule.v1.MyService/* {
+ reverse_proxy mymodule-grpc:8081 {
+ transport http {
+ versions h2c 2
+ }
+ }
+ }
+
+ # Respond 404 if service does not exists
+ handle /api/* {
+ respond "Not Found" 404
+ }
+}
diff --git a/tests/e2e/chainsaw/26-gatewaygrpcapi-sync/chainsaw-test.yaml b/tests/e2e/chainsaw/26-gatewaygrpcapi-sync/chainsaw-test.yaml
new file mode 100644
index 000000000..6e01fa7df
--- /dev/null
+++ b/tests/e2e/chainsaw/26-gatewaygrpcapi-sync/chainsaw-test.yaml
@@ -0,0 +1,99 @@
+apiVersion: chainsaw.kyverno.io/v1alpha1
+kind: Test
+metadata:
+ name: gateway-grpcapi-sync
+ labels:
+ suite: modules
+ feature: gateway-grpcapi-sync
+spec:
+ concurrent: false
+ steps:
+ - name: gateway-renders-grpcapi-routes
+ try:
+ - apply:
+ file: resources/stack.yaml
+ - apply:
+ file: resources/gateway.yaml
+ - apply:
+ file: resources/httpapi-ledger.yaml
+ - apply:
+ file: resources/grpcapi.yaml
+ - script:
+ timeout: 2m
+ content: |
+ set -eu
+ # Wait for GatewayGRPCAPI to be ready
+ kubectl wait --for=jsonpath='{.status.ready}'=true gatewaygrpcapi/chainsaw-grpcapi-mymodule --timeout=2m
+
+ # Verify the -grpc service was created
+ test "$(kubectl get service mymodule-grpc -n chainsaw-grpcapi-sync -o jsonpath='{.spec.ports[0].port}')" = "8081"
+ test "$(kubectl get service mymodule-grpc -n chainsaw-grpcapi-sync -o jsonpath='{.spec.ports[0].name}')" = "grpc"
+ test "$(kubectl get service mymodule-grpc -n chainsaw-grpcapi-sync -o jsonpath='{.spec.selector.app\.kubernetes\.io/name}')" = "mymodule"
+
+ # Verify Gateway synced the GRPCAPIs
+ kubectl wait --for=jsonpath='{.status.syncGRPCAPIs[0]}'=mymodule gateway/chainsaw-grpcapi-gateway --timeout=2m
+
+ # Verify Caddyfile has h2c protocols and gRPC routes
+ kubectl get configmap gateway -n chainsaw-grpcapi-sync -o jsonpath='{.data.Caddyfile}' > /tmp/chainsaw-grpcapi-caddyfile
+ grep -F 'protocols h1 h2c' /tmp/chainsaw-grpcapi-caddyfile
+ grep -F 'handle /formance.mymodule.v1.MyService/*' /tmp/chainsaw-grpcapi-caddyfile
+ grep -F 'handle /formance.mymodule.v1.HealthService/*' /tmp/chainsaw-grpcapi-caddyfile
+ grep -F 'reverse_proxy mymodule-grpc:8081' /tmp/chainsaw-grpcapi-caddyfile
+ grep -F 'versions h2c 2' /tmp/chainsaw-grpcapi-caddyfile
+ catch:
+ - script:
+ timeout: 2m
+ content: ../../scripts/dump-diagnostics.sh gateway-grpcapi-routes
+ - name: grpcapi-mutation-reconfigures-gateway
+ try:
+ - apply:
+ file: resources/grpcapi-updated.yaml
+ - script:
+ timeout: 2m
+ content: |
+ set -eu
+ # Wait until new gRPC service name appears in config
+ until kubectl get configmap gateway -n chainsaw-grpcapi-sync -o jsonpath='{.data.Caddyfile}' | grep -F 'handle /formance.mymodule.v2.MyService/*'; do
+ sleep 1
+ done
+ kubectl get configmap gateway -n chainsaw-grpcapi-sync -o jsonpath='{.data.Caddyfile}' > /tmp/chainsaw-grpcapi-caddyfile
+ # New port should be reflected
+ grep -F 'reverse_proxy mymodule-grpc:9090' /tmp/chainsaw-grpcapi-caddyfile
+ # Old v1 service should be gone
+ if grep -F 'formance.mymodule.v1.MyService' /tmp/chainsaw-grpcapi-caddyfile; then
+ echo "old gRPC service name should not remain after GatewayGRPCAPI mutation"
+ exit 1
+ fi
+ # Service port should be updated too
+ test "$(kubectl get service mymodule-grpc -n chainsaw-grpcapi-sync -o jsonpath='{.spec.ports[0].port}')" = "9090"
+ catch:
+ - script:
+ timeout: 2m
+ content: ../../scripts/dump-diagnostics.sh gateway-grpcapi-mutation
+ - name: grpcapi-deletion-reconfigures-gateway
+ try:
+ - script:
+ timeout: 5m
+ content: |
+ set -eu
+ kubectl delete gatewaygrpcapi chainsaw-grpcapi-mymodule
+ # Service should be deleted
+ kubectl wait --for=delete service/mymodule-grpc -n chainsaw-grpcapi-sync --timeout=3m
+ # Gateway should no longer list grpc APIs
+ until test "$(kubectl get gateway chainsaw-grpcapi-gateway -o jsonpath='{.status.syncGRPCAPIs}')" = "" -o "$(kubectl get gateway chainsaw-grpcapi-gateway -o jsonpath='{.status.syncGRPCAPIs}')" = "[]"; do
+ sleep 1
+ done
+ # Caddyfile should not contain gRPC routes or h2c anymore
+ kubectl get configmap gateway -n chainsaw-grpcapi-sync -o jsonpath='{.data.Caddyfile}' > /tmp/chainsaw-grpcapi-caddyfile
+ if grep -F 'mymodule-grpc' /tmp/chainsaw-grpcapi-caddyfile; then
+ echo "deleted GatewayGRPCAPI should not remain in gateway Caddyfile"
+ exit 1
+ fi
+ if grep -F 'protocols h1 h2c' /tmp/chainsaw-grpcapi-caddyfile; then
+ echo "h2c protocols should be removed when no gRPC APIs exist"
+ exit 1
+ fi
+ catch:
+ - script:
+ timeout: 2m
+ content: ../../scripts/dump-diagnostics.sh gateway-grpcapi-deletion
diff --git a/tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/gateway.yaml b/tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/gateway.yaml
new file mode 100644
index 000000000..a4efa7759
--- /dev/null
+++ b/tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/gateway.yaml
@@ -0,0 +1,7 @@
+apiVersion: formance.com/v1beta1
+kind: Gateway
+metadata:
+ name: chainsaw-grpcapi-gateway
+spec:
+ stack: chainsaw-grpcapi-sync
+ dev: true
diff --git a/tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/grpcapi-updated.yaml b/tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/grpcapi-updated.yaml
new file mode 100644
index 000000000..38c1d4422
--- /dev/null
+++ b/tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/grpcapi-updated.yaml
@@ -0,0 +1,10 @@
+apiVersion: formance.com/v1beta1
+kind: GatewayGRPCAPI
+metadata:
+ name: chainsaw-grpcapi-mymodule
+spec:
+ stack: chainsaw-grpcapi-sync
+ name: mymodule
+ grpcServices:
+ - formance.mymodule.v2.MyService
+ port: 9090
diff --git a/tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/grpcapi.yaml b/tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/grpcapi.yaml
new file mode 100644
index 000000000..36a6448d9
--- /dev/null
+++ b/tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/grpcapi.yaml
@@ -0,0 +1,11 @@
+apiVersion: formance.com/v1beta1
+kind: GatewayGRPCAPI
+metadata:
+ name: chainsaw-grpcapi-mymodule
+spec:
+ stack: chainsaw-grpcapi-sync
+ name: mymodule
+ grpcServices:
+ - formance.mymodule.v1.MyService
+ - formance.mymodule.v1.HealthService
+ port: 8081
diff --git a/tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/httpapi-ledger.yaml b/tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/httpapi-ledger.yaml
new file mode 100644
index 000000000..d9dbad967
--- /dev/null
+++ b/tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/httpapi-ledger.yaml
@@ -0,0 +1,8 @@
+apiVersion: formance.com/v1beta1
+kind: GatewayHTTPAPI
+metadata:
+ name: chainsaw-grpcapi-httpapi-ledger
+spec:
+ stack: chainsaw-grpcapi-sync
+ name: ledger
+ rules: []
diff --git a/tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/stack.yaml b/tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/stack.yaml
new file mode 100644
index 000000000..a9cce67d7
--- /dev/null
+++ b/tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/stack.yaml
@@ -0,0 +1,6 @@
+apiVersion: formance.com/v1beta1
+kind: Stack
+metadata:
+ name: chainsaw-grpcapi-sync
+spec:
+ version: v0.0.0-e2e