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