From e0059ec712b0fd3cabe322f87d38693646a555dd Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 25 May 2026 15:51:08 +0200 Subject: [PATCH 1/5] feat(gateway): add GatewayGRPCAPI CRD for gRPC service exposure Introduce a new GatewayGRPCAPI custom resource that allows modules to expose gRPC services through the gateway, following the same pattern as GatewayHTTPAPI. gRPC routing uses fully-qualified protobuf service names (e.g. formance.ledger.v1.LedgerService) instead of HTTP path prefixes. Traffic is served on the same port (8080) with h2c protocol enabled conditionally when gRPC APIs are registered. Each GatewayGRPCAPI creates a dedicated -grpc Kubernetes Service pointing to the module's gRPC port, avoiding conflicts with existing HTTP services. Co-Authored-By: Claude Opus 4.6 (1M context) --- api/formance.com/v1beta1/gateway_types.go | 3 + .../v1beta1/gatewaygrpcapi_types.go | 88 +++++++++++ .../v1beta1/zz_generated.deepcopy.go | 101 +++++++++++++ .../bases/formance.com_gatewaygrpcapis.yaml | 140 +++++++++++++++++ config/crd/bases/formance.com_gateways.yaml | 5 + config/crd/kustomization.yaml | 1 + config/rbac/role.yaml | 3 + .../02-Custom Resource Definitions.md | 90 +++++++++++ .../settings.catalog.json | 10 +- ...finition_gatewaygrpcapis.formance.com.yaml | 143 ++++++++++++++++++ ...ourcedefinition_gateways.formance.com.yaml | 5 + ..._v1_clusterrole_formance-manager-role.yaml | 3 + internal/resources/all.go | 1 + internal/resources/gatewaygrpcapis/create.go | 45 ++++++ internal/resources/gatewaygrpcapis/init.go | 54 +++++++ internal/resources/gateways/Caddyfile.gotpl | 15 ++ internal/resources/gateways/caddyfile.go | 6 +- internal/resources/gateways/configuration.go | 5 +- internal/resources/gateways/init.go | 17 ++- internal/tests/gateway_controller_test.go | 32 ++++ .../tests/gatewaygrpcapi_controller_test.go | 78 ++++++++++ .../configmap-with-ledger-and-grpc.yaml | 64 ++++++++ .../26-gatewaygrpcapi-sync/chainsaw-test.yaml | 99 ++++++++++++ .../resources/gateway.yaml | 7 + .../resources/grpcapi-updated.yaml | 10 ++ .../resources/grpcapi.yaml | 11 ++ .../resources/httpapi-ledger.yaml | 8 + .../resources/stack.yaml | 6 + 28 files changed, 1041 insertions(+), 9 deletions(-) create mode 100644 api/formance.com/v1beta1/gatewaygrpcapi_types.go create mode 100644 config/crd/bases/formance.com_gatewaygrpcapis.yaml create mode 100644 helm/crds/templates/crds/apiextensions.k8s.io_v1_customresourcedefinition_gatewaygrpcapis.formance.com.yaml create mode 100644 internal/resources/gatewaygrpcapis/create.go create mode 100644 internal/resources/gatewaygrpcapis/init.go create mode 100644 internal/tests/gatewaygrpcapi_controller_test.go create mode 100644 internal/tests/testdata/resources/gateway-controller/configmap-with-ledger-and-grpc.yaml create mode 100644 tests/e2e/chainsaw/26-gatewaygrpcapi-sync/chainsaw-test.yaml create mode 100644 tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/gateway.yaml create mode 100644 tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/grpcapi-updated.yaml create mode 100644 tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/grpcapi.yaml create mode 100644 tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/httpapi-ledger.yaml create mode 100644 tests/e2e/chainsaw/26-gatewaygrpcapi-sync/resources/stack.yaml 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/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..c2a2e2803 --- /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: 2m + 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=2m + # 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 From 3971821fc76f4af2b79e8653f5d0e7bc89dff249 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 25 May 2026 16:05:11 +0200 Subject: [PATCH 2/5] fix(e2e): increase chainsaw grpcapi deletion step timeout The deletion step was timing out on K8s 1.34 because the script timeout (2m) was being consumed by kubectl wait --for=delete. Increase the script timeout to 5m and the wait to 3m for more headroom. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/e2e/chainsaw/26-gatewaygrpcapi-sync/chainsaw-test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/chainsaw/26-gatewaygrpcapi-sync/chainsaw-test.yaml b/tests/e2e/chainsaw/26-gatewaygrpcapi-sync/chainsaw-test.yaml index c2a2e2803..6e01fa7df 100644 --- a/tests/e2e/chainsaw/26-gatewaygrpcapi-sync/chainsaw-test.yaml +++ b/tests/e2e/chainsaw/26-gatewaygrpcapi-sync/chainsaw-test.yaml @@ -73,12 +73,12 @@ spec: - name: grpcapi-deletion-reconfigures-gateway try: - script: - timeout: 2m + 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=2m + 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 From de1e325891a9fc0946bb21e02ddc98ab75f14a8e Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 28 May 2026 17:24:23 +0200 Subject: [PATCH 3/5] feat: add Dockerfile and Pulumi deployment app Extract the operator image build from Earthly into a standard Dockerfile so it can be used by both Earthly (via FROM DOCKERFILE) and the new Pulumi deployment app. The Pulumi app (deployment/operator/) builds the operator image, applies CRDs, and deploys the operator via its Helm chart. Co-Authored-By: Claude Opus 4.6 (1M context) --- Dockerfile | 34 +++ Earthfile | 10 +- deployment/operator/.gitignore | 2 + deployment/operator/Pulumi.yaml | 68 ++++++ deployment/operator/go.mod | 121 +++++++++++ deployment/operator/go.sum | 362 ++++++++++++++++++++++++++++++++ deployment/operator/helpers.go | 296 ++++++++++++++++++++++++++ deployment/operator/main.go | 124 +++++++++++ 8 files changed, 1014 insertions(+), 3 deletions(-) create mode 100644 Dockerfile create mode 100644 deployment/operator/.gitignore create mode 100644 deployment/operator/Pulumi.yaml create mode 100644 deployment/operator/go.mod create mode 100644 deployment/operator/go.sum create mode 100644 deployment/operator/helpers.go create mode 100644 deployment/operator/main.go diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..ac502b218 --- /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 -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/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..480d61281 --- /dev/null +++ b/deployment/operator/helpers.go @@ -0,0 +1,296 @@ +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" + v1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1" + metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1" + "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 +} + +type k8sSetup struct { + Provider pulumi.ProviderResource + Namespace *v1.Namespace +} + +func newK8sSetup(ctx *pulumi.Context, cfg *config.Config) (*k8sSetup, error) { + kubeContext := cfg.Require("k8s-context") + + namespaceName := cfg.Get("namespace") + if namespaceName == "" { + namespaceName = ctx.Stack() + } + + 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) + } + + namespace, err := v1.NewNamespace(ctx, "namespace", &v1.NamespaceArgs{ + Metadata: &metav1.ObjectMetaArgs{ + Name: pulumi.String(namespaceName), + }, + }, pulumi.Provider(k8sProvider), pulumi.RetainOnDelete(true)) + if err != nil { + return nil, fmt.Errorf("failed to create namespace: %w", err) + } + + return &k8sSetup{ + Provider: k8sProvider, + Namespace: namespace, + }, 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..5ae98fec4 --- /dev/null +++ b/deployment/operator/main.go @@ -0,0 +1,124 @@ +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, "") + + k8s, err := newK8sSetup(ctx, cfg) + if err != nil { + return err + } + namespace := k8s.Namespace + k8sProvider := k8s.Provider + + 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: namespace.Metadata.Name(), + 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{namespace, operatorImage.Resource()}, crds...)), + pulumi.Provider(k8sProvider), + ) + if err != nil { + return fmt.Errorf("failed to deploy operator: %w", err) + } + + // Exports + ctx.Export("namespace", namespace.Metadata.Name()) + ctx.Export("operatorImage", pulumi.Sprintf("%s/formancehq/operator:latest@%s", dc.PullRegistry, operatorImage.Digest)) + ctx.Export("operatorRelease", operatorRelease.Name) + + return nil + }) +} From c0fdbff965fd45264fba5f571e7d8efc4f3895d5 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 28 May 2026 17:27:59 +0200 Subject: [PATCH 4/5] refactor(pulumi): take namespace as parameter instead of creating it The Helm release creates the namespace itself via createNamespace. Defaults to formance-system. Co-Authored-By: Claude Opus 4.6 (1M context) --- deployment/operator/helpers.go | 28 ++-------------------------- deployment/operator/main.go | 16 ++++++++++------ 2 files changed, 12 insertions(+), 32 deletions(-) diff --git a/deployment/operator/helpers.go b/deployment/operator/helpers.go index 480d61281..44effc7eb 100644 --- a/deployment/operator/helpers.go +++ b/deployment/operator/helpers.go @@ -10,8 +10,6 @@ import ( "github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild" "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes" - v1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1" - metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1" "github.com/pulumi/pulumi/sdk/v3/go/pulumi" "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config" "gopkg.in/yaml.v3" @@ -52,19 +50,9 @@ func getConfigBool(cfg *config.Config, key string, fallback bool) bool { return fallback } -type k8sSetup struct { - Provider pulumi.ProviderResource - Namespace *v1.Namespace -} - -func newK8sSetup(ctx *pulumi.Context, cfg *config.Config) (*k8sSetup, error) { +func newK8sProvider(ctx *pulumi.Context, cfg *config.Config) (pulumi.ProviderResource, error) { kubeContext := cfg.Require("k8s-context") - namespaceName := cfg.Get("namespace") - if namespaceName == "" { - namespaceName = ctx.Stack() - } - k8sProvider, err := kubernetes.NewProvider(ctx, "k8s", &kubernetes.ProviderArgs{ Context: pulumi.StringPtr(kubeContext), }) @@ -72,19 +60,7 @@ func newK8sSetup(ctx *pulumi.Context, cfg *config.Config) (*k8sSetup, error) { return nil, fmt.Errorf("failed to create k8s provider: %w", err) } - namespace, err := v1.NewNamespace(ctx, "namespace", &v1.NamespaceArgs{ - Metadata: &metav1.ObjectMetaArgs{ - Name: pulumi.String(namespaceName), - }, - }, pulumi.Provider(k8sProvider), pulumi.RetainOnDelete(true)) - if err != nil { - return nil, fmt.Errorf("failed to create namespace: %w", err) - } - - return &k8sSetup{ - Provider: k8sProvider, - Namespace: namespace, - }, nil + return k8sProvider, nil } type dockerConfig struct { diff --git a/deployment/operator/main.go b/deployment/operator/main.go index 5ae98fec4..0c8ddb1c0 100644 --- a/deployment/operator/main.go +++ b/deployment/operator/main.go @@ -15,12 +15,15 @@ func main() { pulumi.Run(func(ctx *pulumi.Context) error { cfg := config.New(ctx, "") - k8s, err := newK8sSetup(ctx, cfg) + k8sProvider, err := newK8sProvider(ctx, cfg) if err != nil { return err } - namespace := k8s.Namespace - k8sProvider := k8s.Provider + + namespace := cfg.Get("namespace") + if namespace == "" { + namespace = "formance-system" + } dc := newDockerConfig(ctx, cfg) @@ -83,7 +86,8 @@ func main() { operatorRelease, err := helm.NewRelease(ctx, "formance-operator", &helm.ReleaseArgs{ Name: pulumi.String("formance-operator"), Chart: pulumi.String(operatorChartPath), - Namespace: namespace.Metadata.Name(), + Namespace: pulumi.String(namespace), + CreateNamespace: pulumi.Bool(true), Values: pulumi.Map{ "operator-crds": pulumi.Map{ "create": pulumi.Bool(false), @@ -107,7 +111,7 @@ func main() { }, ForceUpdate: pulumi.Bool(true), }, - pulumi.DependsOn(append([]pulumi.Resource{namespace, operatorImage.Resource()}, crds...)), + pulumi.DependsOn(append([]pulumi.Resource{operatorImage.Resource()}, crds...)), pulumi.Provider(k8sProvider), ) if err != nil { @@ -115,7 +119,7 @@ func main() { } // Exports - ctx.Export("namespace", namespace.Metadata.Name()) + 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) From 6aa2687ebd98684755eee2e3f293deeda07d19c6 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 28 May 2026 17:40:08 +0200 Subject: [PATCH 5/5] fix: add -buildvcs=false to Dockerfile go build The Docker build context may not include .git, causing go build to fail with "error obtaining VCS status". Disable VCS stamping since version info is already passed via ldflags. Co-Authored-By: Claude Opus 4.6 (1M context) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index ac502b218..6733ff7d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,7 @@ RUN set -e; \ 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 -o /usr/bin/operator -ldflags="${LDFLAGS}" . + CGO_ENABLED=0 go build -buildvcs=false -o /usr/bin/operator -ldflags="${LDFLAGS}" . FROM alpine:3.20