diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index fe6d97ae..b17e6284 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -11,7 +11,7 @@ defaults: shell: bash env: - GOLANG_CI_VERSION: v1.64.8 + GOLANG_CI_VERSION: v2.12.2 jobs: license: @@ -31,9 +31,21 @@ jobs: go-version-file: 'go.mod' cache: false - name: lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v9 with: version: ${{ env.GOLANG_CI_VERSION }} + # A restored analysis cache produces incorrect results. Two runs of + # this workflow over a byte-identical main.go disagreed: the first, + # which populated the cache, reported no issues; the second, which + # restored it, reported the two SA1019 //nolint directives in main.go + # as unused. Removing those directives and running cold shows SA1019 + # firing at both sites, so the directives are used and the cached + # verdict was wrong. It fails in the other direction too — a warm + # cache locally reported "0 issues" on a tree that genuinely had one. + # + # Correct lint results matter more than the ~1 minute the cache saves + # (the cached run still took 130s), so every run starts cold. + skip-cache: true build: name: Build operator runs-on: 'ubuntu-latest' @@ -56,3 +68,14 @@ jobs: check-latest: true - name: test run: make test + chart-crds: + name: Check chart CRDs are in sync + runs-on: 'ubuntu-latest' + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version-file: 'go.mod' + check-latest: true + - name: verify + run: make verify-chart-crds diff --git a/.golangci.yaml b/.golangci.yaml index 7f9b0a43..db0120f6 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,133 +1,170 @@ +version: "2" run: - timeout: 10m - go: "1.22" + # Language version is derived from go.mod. Do not pin it here: a stale pin + # silently holds linters to older semantics than the module actually targets. allow-parallel-runners: true - linters: - disable-all: true + default: none enable: - - asasalint - - asciicheck - - bidichk - - bodyclose - - containedctx - - dogsled - - dupword - - durationcheck - - errcheck - - errchkjson - - copyloopvar - - ginkgolinter - - goconst - - gocritic - - godot - - gofmt - - goimports - - goprintffuncname - - gosec - - gosimple - - govet - - importas - - ineffassign - - misspell - - nakedret - - nilerr - - noctx - - nolintlint - - nosprintfhostport - - prealloc - - predeclared - - revive - - rowserrcheck - - staticcheck - - stylecheck - - typecheck - - unconvert - - unused - - usestdlibvars - - whitespace - - unparam - -linters-settings: - ginkgolinter: - # Suppress the wrong length assertion warning. - suppress-len-assertion: false - # Suppress the wrong nil assertion warning. - suppress-nil-assertion: false - # Suppress the wrong error assertion warning. - suppress-err-assertion: true - stylecheck: - checks: ["all", "-ST1000", "-ST1020"] - importas: - no-unaliased: true - alias: - # Kubernetes - - pkg: k8s.io/api/core/v1 - alias: corev1 - - pkg: k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1 - alias: apiextensionsv1 - - pkg: k8s.io/apimachinery/pkg/apis/meta/v1 - alias: metav1 - - pkg: k8s.io/apimachinery/pkg/api/errors - alias: apierrors - - pkg: k8s.io/apimachinery/pkg/util/errors - alias: kerrors - - pkg: k8s.io/api/apps/v1 - alias: appsv1 - - pkg: k8s.io/api/batch/v1 - alias: batchv1 - - pkg: k8s.io/api/networking/v1 - alias: networkingv1 - # Cert Manager - - pkg: github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 - alias: certmanagerv1 - # Prometheus Operator - - pkg: github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1 - alias: monitoringv1 - # Istio - - pkg: istio.io/client-go/pkg/apis/networking/v1beta1 - alias: istionetworkingv1beta1 - - pkg: istio.io/client-go/pkg/apis/security/v1beta1 - alias: istiosecurityv1beta1 - - pkg: istio.io/api/security/v1beta1 - alias: istioapisecurityv1beta1 - - pkg: istio.io/api/type/v1beta1 - alias: istioapiv1beta1 - # Controller Runtime - - pkg: sigs.k8s.io/controller-runtime - alias: ctrl - nolintlint: - allow-unused: false - require-specific: true - revive: - rules: - - name: blank-imports - - name: context-as-argument - - name: context-keys-type - - name: error-return - - name: error-strings - - name: error-naming - - name: if-return - - name: increment-decrement - - name: var-naming - - name: var-declaration - - name: range - - name: receiver-naming - - name: time-naming - - name: unexported-return - - name: indent-error-flow - - name: errorf - - name: empty-block - - name: superfluous-else - - name: unreachable-code - - name: redefines-builtin-id - - name: bool-literal-in-expr - - name: constant-logical-expr - - name: exported - - name: unused-parameter - - name: package-comments + - asasalint + - asciicheck + - bidichk + - bodyclose + - containedctx + - copyloopvar + - dogsled + - dupword + - durationcheck + - errcheck + - errchkjson + - ginkgolinter + - goconst + - gocritic + - godot + - goprintffuncname + - gosec + - govet + - importas + - ineffassign + - misspell + - nakedret + - nilerr + - noctx + - nolintlint + - nosprintfhostport + - prealloc + - predeclared + - revive + - rowserrcheck + - staticcheck + - unconvert + - unparam + - unused + - usestdlibvars + - whitespace + settings: + goconst: + # Table-driven tests legitimately repeat short literals ("test", "secret", + # "password") across cases; hoisting those into constants makes the tables + # harder to read, not easier. + # + # This restores v1's signal rather than suppressing it. Evidence: the v1 + # config carried no test exclusions at all (only zz_generated), and the + # lint job was green on upstream main 1398896 with v1.64.8 — while the + # files this would newly flag (internal/resource/persistence/utils_test.go, + # pkg/status/status_test.go, pkg/kubernetes/overrides_test.go, ...) all + # already existed at that commit. So v1's goconst was not reporting them, + # and without this setting the v2 migration would add ~70 findings that + # have nothing to do with any behaviour change. + ignore-tests: true + govet: + disable: + # Reports "cannot inline: type parameter inference is not yet supported" + # on generic calls such as slices.Contains. That is the analyzer + # describing its own limitation, not a defect in our code, and it only + # started firing once the module moved to go 1.26. + - inline + ginkgolinter: + suppress-len-assertion: false + suppress-nil-assertion: false + suppress-err-assertion: true + importas: + alias: + - pkg: k8s.io/api/core/v1 + alias: corev1 + - pkg: k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1 + alias: apiextensionsv1 + - pkg: k8s.io/apimachinery/pkg/apis/meta/v1 + alias: metav1 + - pkg: k8s.io/apimachinery/pkg/api/errors + alias: apierrors + - pkg: k8s.io/apimachinery/pkg/util/errors + alias: kerrors + - pkg: k8s.io/api/apps/v1 + alias: appsv1 + - pkg: k8s.io/api/batch/v1 + alias: batchv1 + - pkg: k8s.io/api/networking/v1 + alias: networkingv1 + - pkg: github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 + alias: certmanagerv1 + - pkg: github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1 + alias: monitoringv1 + - pkg: istio.io/client-go/pkg/apis/networking/v1beta1 + alias: istionetworkingv1beta1 + - pkg: istio.io/client-go/pkg/apis/security/v1beta1 + alias: istiosecurityv1beta1 + - pkg: istio.io/api/security/v1beta1 + alias: istioapisecurityv1beta1 + - pkg: istio.io/api/type/v1beta1 + alias: istioapiv1beta1 + - pkg: sigs.k8s.io/controller-runtime + alias: ctrl + no-unaliased: true + nolintlint: + require-specific: true + allow-unused: false + revive: + rules: + - name: blank-imports + - name: context-as-argument + - name: context-keys-type + - name: error-return + - name: error-strings + - name: error-naming + - name: if-return + - name: increment-decrement + - name: var-naming + - name: var-declaration + - name: range + - name: receiver-naming + - name: time-naming + - name: unexported-return + - name: indent-error-flow + - name: errorf + - name: empty-block + - name: superfluous-else + - name: unreachable-code + - name: redefines-builtin-id + - name: bool-literal-in-expr + - name: constant-logical-expr + - name: exported + - name: unused-parameter + - name: package-comments + staticcheck: + # v1 applied this list to `stylecheck`, where "all" meant the ST* checks. + # v2 merged gosimple/stylecheck/typecheck into staticcheck, so "all" now + # also pulls in the QF* ("quickfix") suggestions, which v1 never ran. + # Excluding QF* keeps the enabled set equivalent to the pre-migration one. + checks: + - all + - -ST1000 + - -ST1020 + - -QF1001 + - -QF1008 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - zz_generated.*\.go$ + - third_party$ + - builtin$ + - examples$ issues: - max-same-issues: 0 max-issues-per-linter: 0 - exclude-files: - - "zz_generated.*\\.go$" \ No newline at end of file + max-same-issues: 0 +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - zz_generated.*\.go$ + - third_party$ + - builtin$ + - examples$ diff --git a/CHANGELOG.md b/CHANGELOG.md index 64b61fc9..722c1bc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,30 @@ All notable changes to this project are documented in this file. +## Unreleased + +Improvements: +- Add support for Temporal Server v1.29.x. Temporal v1.29 introduces only dynamic-config changes (task-queue fairness, task-queue config API), which are already supported through the cluster `dynamicConfig` field. +- Add support for Temporal Server v1.30.x. (The defaults and supported range moved on again with v1.31 support below; see that entry for the values this release actually ships.) + - Temporal v1.30 removed `dockerize`/`auto-setup` from the `temporalio/server` image and moved config-template rendering into the server binary (embedded sprig engine). For clusters running `>= 1.30`, the operator now emits config templates with the `# enable-template` header and sprig `{{ env "NAME" }}` placeholders (instead of the dockerize `{{ .Env.NAME }}` syntax), sets `TEMPORAL_SERVER_CONFIG_FILE_PATH`, and selects the service to start through the new `TEMPORAL_SERVICES` environment variable (the legacy `SERVICES` variable is still set for backward compatibility). + - Temporal v1.30 also removed `curl` and `jq` from the `temporalio/admin-tools` image, which broke the operator's Elasticsearch visibility setup scripts. For clusters `>= 1.30` the operator now drives ES visibility setup/upgrade through the `temporal-elasticsearch-tool` shipped in the image (`setup-schema`, `create-index`, `update-schema`), analogous to `temporal-sql-tool`. Its embedded index template applies all built-in search attributes automatically. The MTLS sidecar-shutdown step now uses `wget` instead of `curl` on `>= 1.30`. Clusters `< 1.30` keep the previous `curl`-based scripts. +- Broken releases: `v1.30.0` has no published GitHub release upstream (silently skipped) and is now rejected; use `v1.30.1+`. +- Add support for Temporal Server v1.31.x. The default Temporal version is now `1.31.1`, the default Temporal UI version is now `2.49.1`, and the supported version range is extended to `< 1.32.0`. + - New `sql.passwordCommand` field on datastores (Temporal >= 1.31): resolves the datastore password by running an external command, e.g. to generate a short-lived cloud IAM auth token (AWS RDS / GCP Cloud SQL). Mutually exclusive with `passwordSecretRef`; validated by the webhook. The password is wired both into the server config (native support) and into the persistence schema-setup jobs, where the generated `temporal-sql-tool` invocation resolves it through a shell command substitution. + + **Known limitation:** the schema-setup jobs run the command inside the `admin-tools` image, and their pod spec is fully operator-owned — there is no volume or container override through which a helper binary could be supplied. The command must therefore already exist in that image. If it does not, the server pods resolve the password correctly but schema setup fails with a password-authentication error. The webhook emits an admission warning to this effect. Extending the schema jobs with pod-level overrides is tracked separately. + - Elasticsearch visibility on `>= 1.31` uses the `temporal-elasticsearch-tool` path introduced for `>= 1.30` (see the 1.30 entry); its embedded index template applies all built-in search attributes up to v14 (including `TemporalExternalPayloadSizeBytes`/`TemporalExternalPayloadCount`) automatically. + +Fixes: +- Preserve externally-added pod-template labels and annotations (e.g. `kubectl.kubernetes.io/restartedAt` from `kubectl rollout restart`) across reconciles, while still removing operator-managed ones when the spec stops asking for them. Disabling a feature now actually clears its metadata: previously, clearing `spec.mTLS` left `sidecar.istio.io/inject: "true"` behind and istio kept injecting sidecars, and turning off `spec.metrics` left the `prometheus.io/*` scrape annotations in place. +- `spec.version` values that are marked broken no longer suggest another broken release as the upgrade target (`1.26.0` previously suggested `1.26.1`, which is also rejected). +- Dynamic config integers written in exponent form (e.g. `1e9`) are no longer emitted in scientific notation, which Temporal's file-based dynamic config client rejects for settings expecting an integer. Large integers no longer truncate on 32-bit builds. + +Updates: +- Bump `go.temporal.io/server` to v1.31.1, `go.temporal.io/api` to v1.62.8, `go.temporal.io/sdk` to v1.41.1. +- Bump `controller-gen` to v0.21.0 (v0.16.3 cannot be built with Go 1.26). **This changes the published CRD schema for two pre-existing fields:** `cassandra.consistency` and `cassandra.serialConsistency` are now `type: string` instead of `type: integer`. The previous schema was self-contradictory — it declared `type: integer` alongside string enum values (`ANY`, `ONE`, `LOCAL_QUORUM`, ...), so no value could ever validate. `type: string` matches the JSON form these fields have always had, since `gocql.Consistency` implements `encoding.TextMarshaler`. No spec change is required of users. +- Bump `controller-runtime` to v0.23.3 (pairs with client-go v0.35) and migrate `golangci-lint` to v2. + ## 0.12.2 **Release date:** 2023-04-02 diff --git a/Dockerfile b/Dockerfile index 12c66ccc..9551fdc7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM --platform=${BUILDPLATFORM} golang:1.24.6 AS builder +FROM --platform=${BUILDPLATFORM} golang:1.26.4 AS builder ARG TARGETPLATFORM ARG BUILDPLATFORM diff --git a/Makefile b/Makefile index 7eff7c04..ae70ba1a 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,16 @@ endif VERSION ?= "$(shell cat VERSION)" +# Container tool used by the local-development targets. Prefers podman when it +# is installed and falls back to docker, so a podman-only machine works without +# extra flags while CI (docker) is unaffected. Override with CONTAINER_TOOL=... +CONTAINER_TOOL ?= $(shell command -v podman >/dev/null 2>&1 && echo podman || echo docker) + +# Wall-clock budget for the end-to-end suite. TestPersistence walks every +# supported upgrade path for six datastore configurations, which is the bulk of +# it; see the comment in tests/e2e/persistence_test.go. +E2E_TIMEOUT ?= 90m + # Setting SHELL to bash allows bash commands to be executed by recipes. # This is a requirement for 'setup-envtest.sh' in the test target. # Options are set to exit when a recipe line exits non-zero or a piped command fails. @@ -98,13 +108,13 @@ test: manifests generate fmt vet envtest ## Run tests. .PHONY: test-e2e test-e2e: artifacts ## Run end2end tests. - go test ./tests/e2e -v -timeout 60m -args "--v=4" + go test ./tests/e2e -v -timeout $(E2E_TIMEOUT) -args "--v=4" .PHONY: test-e2e-dev test-e2e-dev: artifacts ## Run end2end tests on dev computer using kind. - docker build -t temporal-operator . - docker save temporal-operator > /tmp/temporal-operator.tar - OPERATOR_IMAGE_PATH=/tmp/temporal-operator.tar go test ./tests/e2e -v -timeout 60m -args "-v=4" + $(CONTAINER_TOOL) build -t temporal-operator . + $(CONTAINER_TOOL) save temporal-operator > /tmp/temporal-operator.tar + OPERATOR_IMAGE_PATH=/tmp/temporal-operator.tar go test ./tests/e2e -v -timeout $(E2E_TIMEOUT) -args "-v=4" .PHONY: ensure-license ensure-license: go-licenser @@ -138,7 +148,7 @@ run: manifests generate fmt vet ## Run a controller from your host. .PHONY: docker-build-dev docker-build-dev: ## Build docker image with the manager. - docker build -t temporal-operator . + $(CONTAINER_TOOL) build -t temporal-operator . ##@ Deployment @@ -174,6 +184,19 @@ helm: helm-docs manifests artifacts cp ${RELEASE_PATH}/temporal-operator.crds.yaml charts/temporal-operator/crds $(HELM_DOCS) --chart-search-root=charts/temporal-operator --template-files=hack/helm/template/README.md.gotmpl +.PHONY: verify-chart-crds +verify-chart-crds: kustomize ## Verify the chart's bundled CRDs are in sync with config/crd. + @generated=$$(mktemp); \ + $(KUSTOMIZE) build config/crd > $$generated; \ + if ! diff -u charts/temporal-operator/crds/temporal-operator.crds.yaml $$generated; then \ + rm -f $$generated; \ + echo ""; \ + echo "ERROR: charts/temporal-operator/crds/temporal-operator.crds.yaml is out of sync with config/crd."; \ + echo "Regenerate it with: make artifacts && cp out/release/artifacts/temporal-operator.crds.yaml charts/temporal-operator/crds/"; \ + exit 1; \ + fi; \ + rm -f $$generated + .PHONY: bundle bundle: manifests kustomize operator-sdk ## Generate bundle manifests and metadata, then validate generated files. $(OPERATOR_SDK) generate kustomize manifests -q @@ -216,10 +239,10 @@ HELM_DOCS ?= $(LOCALBIN)/helm-docs ## Tool Versions KUSTOMIZE_VERSION ?= v4.5.7 OPERATOR_SDK_VERSION ?= 1.37.0 -CONTROLLER_TOOLS_VERSION ?= v0.16.3 +CONTROLLER_TOOLS_VERSION ?= v0.21.0 GO_LICENSER_VERSION ?= v0.4.0 GEN_CRD_API_REFERENCE_DOCS_VERSION ?= 3f29e6853552dcf08a8e846b1225f275ed0f3e3b -GOLANGCI_LINT_VERSION ?= v1.64.8 +GOLANGCI_LINT_VERSION ?= v2.12.2 YQ_VERSION ?= v4.30.6 KIND_WITH_REGISTRY_VERSION ?= 0.17.0 HELM_DOCS_VERSION ?= v1.12.0 @@ -252,7 +275,7 @@ $(OPERATOR_SDK): $(LOCALBIN) golangci-lint: $(GOLANGCI_LINT) $(GOLANGCI_LINT): $(LOCALBIN) test -s $(LOCALBIN)/golangci-lint && $(LOCALBIN)/golangci-lint version | grep -q $(GOLANGCI_LINT_VERSION) || \ - GOBIN=$(LOCALBIN) go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) + GOBIN=$(LOCALBIN) go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) .PHONY: go-licenser go-licenser: $(GO_LICENSER) diff --git a/README.md b/README.md index ea40c757..02dc814c 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Please note this table only reports end-to-end tests suite coverage, others vers | Temporal Operator | Temporal | Kubernetes | |------------------------|--------------------|----------------| -| v0.22.x (not released) | v1.24.x to v1.28.x | v1.30 to v1.33 | +| v0.22.x (not released) | v1.24.x to v1.31.x | v1.30 to v1.33 | | v0.21.x | v1.20.x to v1.25.x | v1.27 to v1.31 | | v0.20.x | v1.19.x to v1.24.x | v1.26 to v1.30 | | v0.19.x | v1.19.x to v1.23.x | v1.25 to v1.29 | diff --git a/api/v1beta1/condition_types.go b/api/v1beta1/condition_types.go index 7ce7e265..49043e8f 100644 --- a/api/v1beta1/condition_types.go +++ b/api/v1beta1/condition_types.go @@ -38,6 +38,8 @@ const ( ) // SetTemporalClusterReconcileSuccess sets the ReconcileSuccessCondition status for a temporal cluster. +// On a successful reconciliation it also reports the ReconcileErrorCondition as false, +// so that errors from previous reconcile cycles don't stay reported forever. func SetTemporalClusterReconcileSuccess(c *TemporalCluster, status metav1.ConditionStatus, reason, message string) { condition := metav1.Condition{ Type: ReconcileSuccessCondition, @@ -48,6 +50,9 @@ func SetTemporalClusterReconcileSuccess(c *TemporalCluster, status metav1.Condit Message: message, } apimeta.SetStatusCondition(&c.Status.Conditions, condition) + if status == metav1.ConditionTrue { + SetTemporalClusterReconcileError(c, metav1.ConditionFalse, ReconcileSuccessReason, "") + } } // SetTemporalClusterReconcileError sets the ReconcileErrorCondition status for a temporal cluster. @@ -109,6 +114,8 @@ func SetTemporalScheduleReady(s *TemporalSchedule, status metav1.ConditionStatus } // SetTemporalNamespaceReconcileSuccess sets the ReconcileSuccessCondition status for a temporal namespace. +// On a successful reconciliation it also reports the ReconcileErrorCondition as false, +// so that errors from previous reconcile cycles don't stay reported forever. func SetTemporalNamespaceReconcileSuccess(n *TemporalNamespace, status metav1.ConditionStatus, reason, message string) { condition := metav1.Condition{ Type: ReconcileSuccessCondition, @@ -119,9 +126,14 @@ func SetTemporalNamespaceReconcileSuccess(n *TemporalNamespace, status metav1.Co Message: message, } apimeta.SetStatusCondition(&n.Status.Conditions, condition) + if status == metav1.ConditionTrue { + SetTemporalNamespaceReconcileError(n, metav1.ConditionFalse, ReconcileSuccessReason, "") + } } // SetTemporalScheduleReconcileSuccess sets the ReconcileSuccessCondition status for a temporal schedule. +// On a successful reconciliation it also reports the ReconcileErrorCondition as false, +// so that errors from previous reconcile cycles don't stay reported forever. func SetTemporalScheduleReconcileSuccess(s *TemporalSchedule, status metav1.ConditionStatus, reason, message string) { condition := metav1.Condition{ Type: ReconcileSuccessCondition, @@ -132,6 +144,9 @@ func SetTemporalScheduleReconcileSuccess(s *TemporalSchedule, status metav1.Cond Message: message, } apimeta.SetStatusCondition(&s.Status.Conditions, condition) + if status == metav1.ConditionTrue { + SetTemporalScheduleReconcileError(s, metav1.ConditionFalse, ReconcileSuccessReason, "") + } } // SetTemporalNamespaceReconcileError sets the ReconcileErrorCondition status for a temporal namespace. diff --git a/api/v1beta1/condition_types_test.go b/api/v1beta1/condition_types_test.go new file mode 100644 index 00000000..7bca4686 --- /dev/null +++ b/api/v1beta1/condition_types_test.go @@ -0,0 +1,124 @@ +// Licensed to Alexandre VILAIN under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Alexandre VILAIN licenses this file to you 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_test + +import ( + "testing" + + "github.com/alexandrevilain/temporal-operator/api/v1beta1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type reconcileConditionsAccessor struct { + setReconcileError func(status metav1.ConditionStatus, reason, message string) + setReconcileSuccess func(status metav1.ConditionStatus, reason, message string) + setGeneration func(generation int64) + conditions func() []metav1.Condition +} + +func reconcileConditionsAccessors() map[string]reconcileConditionsAccessor { + cluster := &v1beta1.TemporalCluster{} + namespace := &v1beta1.TemporalNamespace{} + schedule := &v1beta1.TemporalSchedule{} + + return map[string]reconcileConditionsAccessor{ + "temporal cluster": { + setReconcileError: func(status metav1.ConditionStatus, reason, message string) { + v1beta1.SetTemporalClusterReconcileError(cluster, status, reason, message) + }, + setReconcileSuccess: func(status metav1.ConditionStatus, reason, message string) { + v1beta1.SetTemporalClusterReconcileSuccess(cluster, status, reason, message) + }, + setGeneration: func(generation int64) { cluster.Generation = generation }, + conditions: func() []metav1.Condition { return cluster.Status.Conditions }, + }, + "temporal namespace": { + setReconcileError: func(status metav1.ConditionStatus, reason, message string) { + v1beta1.SetTemporalNamespaceReconcileError(namespace, status, reason, message) + }, + setReconcileSuccess: func(status metav1.ConditionStatus, reason, message string) { + v1beta1.SetTemporalNamespaceReconcileSuccess(namespace, status, reason, message) + }, + setGeneration: func(generation int64) { namespace.Generation = generation }, + conditions: func() []metav1.Condition { return namespace.Status.Conditions }, + }, + "temporal schedule": { + setReconcileError: func(status metav1.ConditionStatus, reason, message string) { + v1beta1.SetTemporalScheduleReconcileError(schedule, status, reason, message) + }, + setReconcileSuccess: func(status metav1.ConditionStatus, reason, message string) { + v1beta1.SetTemporalScheduleReconcileSuccess(schedule, status, reason, message) + }, + setGeneration: func(generation int64) { schedule.Generation = generation }, + conditions: func() []metav1.Condition { return schedule.Status.Conditions }, + }, + } +} + +func TestSetReconcileSuccessClearsReconcileError(t *testing.T) { + for name, accessor := range reconcileConditionsAccessors() { + t.Run(name, func(t *testing.T) { + accessor.setGeneration(1) + accessor.setReconcileError(metav1.ConditionTrue, v1beta1.ReconcileErrorReason, "the object has been modified") + + accessor.setGeneration(2) + accessor.setReconcileSuccess(metav1.ConditionTrue, v1beta1.ReconcileSuccessReason, "") + + successCondition := apimeta.FindStatusCondition(accessor.conditions(), v1beta1.ReconcileSuccessCondition) + require.NotNil(t, successCondition) + assert.Equal(t, metav1.ConditionTrue, successCondition.Status) + assert.Equal(t, v1beta1.ReconcileSuccessReason, successCondition.Reason) + assert.EqualValues(t, 2, successCondition.ObservedGeneration) + + errorCondition := apimeta.FindStatusCondition(accessor.conditions(), v1beta1.ReconcileErrorCondition) + require.NotNil(t, errorCondition, "ReconcileError condition should be kept, not deleted") + assert.Equal(t, metav1.ConditionFalse, errorCondition.Status) + assert.Equal(t, v1beta1.ReconcileSuccessReason, errorCondition.Reason) + assert.Empty(t, errorCondition.Message) + assert.EqualValues(t, 2, errorCondition.ObservedGeneration) + assert.False(t, errorCondition.LastTransitionTime.IsZero()) + + // A second successful reconcile should not bump the transition time + // of an already false ReconcileError condition. + lastTransitionTime := errorCondition.LastTransitionTime + accessor.setReconcileSuccess(metav1.ConditionTrue, v1beta1.ReconcileSuccessReason, "") + errorCondition = apimeta.FindStatusCondition(accessor.conditions(), v1beta1.ReconcileErrorCondition) + require.NotNil(t, errorCondition) + assert.Equal(t, metav1.ConditionFalse, errorCondition.Status) + assert.Equal(t, lastTransitionTime, errorCondition.LastTransitionTime) + }) + } +} + +func TestSetReconcileSuccessReportsReconcileErrorFalseWhenUnset(t *testing.T) { + for name, accessor := range reconcileConditionsAccessors() { + t.Run(name, func(t *testing.T) { + accessor.setGeneration(1) + accessor.setReconcileSuccess(metav1.ConditionTrue, v1beta1.ReconcileSuccessReason, "") + + errorCondition := apimeta.FindStatusCondition(accessor.conditions(), v1beta1.ReconcileErrorCondition) + require.NotNil(t, errorCondition) + assert.Equal(t, metav1.ConditionFalse, errorCondition.Status) + assert.Equal(t, v1beta1.ReconcileSuccessReason, errorCondition.Reason) + assert.EqualValues(t, 1, errorCondition.ObservedGeneration) + }) + } +} diff --git a/api/v1beta1/temporalcluster_defaults.go b/api/v1beta1/temporalcluster_defaults.go index 4bf9d41f..ba3972cf 100644 --- a/api/v1beta1/temporalcluster_defaults.go +++ b/api/v1beta1/temporalcluster_defaults.go @@ -26,10 +26,10 @@ import ( ) const ( - defaultTemporalVersion = "1.24.3" + defaultTemporalVersion = "1.31.1" defaultTemporalImage = "temporalio/server" - defaultTemporalUIVersion = "2.27.3" + defaultTemporalUIVersion = "2.49.1" defaultTemporalUIImage = "temporalio/ui" defaultTemporalAdmintoolsImage = "temporalio/admin-tools" diff --git a/api/v1beta1/temporalcluster_types.go b/api/v1beta1/temporalcluster_types.go index a0677e64..169eba22 100644 --- a/api/v1beta1/temporalcluster_types.go +++ b/api/v1beta1/temporalcluster_types.go @@ -39,7 +39,7 @@ type LogSpec struct { // Stdout is true if the output needs to goto standard out; default is stderr. // +optional // +kubebuilder:default=true - Stdout *bool `json:"stdout"` + Stdout *bool `json:"stdout,omitempty"` // Level is the desired log level; see colocated zap_logger.go::parseZapLevel() // +optional // +kubebuilder:validation:Enum=debug;info;warn;error;dpanic;panic;fatal @@ -71,7 +71,7 @@ type ServiceSpec struct { // 7235 for Matching service // 7239 for Worker service // +optional - Port *int32 `json:"port"` + Port *int32 `json:"port,omitempty"` // MembershipPort defines a custom membership port for the service. // Default values are: // 6933 for Frontend service @@ -79,16 +79,16 @@ type ServiceSpec struct { // 6935 for Matching service // 6939 for Worker service // +optional - MembershipPort *int32 `json:"membershipPort"` + MembershipPort *int32 `json:"membershipPort,omitempty"` // HTTPPort defines a custom http port for the service. // Default values are: // 7243 for Frontend service // +optional - HTTPPort *int32 `json:"httpPort"` + HTTPPort *int32 `json:"httpPort,omitempty"` // Number of desired replicas for the service. Default to 1. // +kubebuilder:validation:Minimum=1 // +optional - Replicas *int32 `json:"replicas"` + Replicas *int32 `json:"replicas,omitempty"` // Compute Resources required by this service. // More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ // +optional @@ -253,6 +253,28 @@ type SQLSpec struct { // GCPServiceAccount is the service account to use to authenticate with GCP CloudSQL. // +optional GCPServiceAccount *string `json:"gcpServiceAccount,omitempty"` + // PasswordCommand executes an external command whose standard output is used + // as the datastore password, for instance to generate a short-lived cloud IAM + // auth token (AWS RDS, GCP Cloud SQL). Mutually exclusive with the datastore + // passwordSecretRef. When the command returns an expiring token, set + // maxConnLifetime so connections are recycled before the token expires. + // Requires Temporal >= 1.31.0. + // +optional + PasswordCommand *SQLPasswordCommandSpec `json:"passwordCommand,omitempty"` +} + +// SQLPasswordCommandSpec configures an external command used to retrieve the +// datastore password at runtime. Available for Temporal clusters >= 1.31.0. +type SQLPasswordCommandSpec struct { + // Command is the path to the executable to run. + Command string `json:"command"` + // Args is the list of arguments passed to the command. + // +optional + Args []string `json:"args,omitempty"` + // Timeout is the maximum duration to wait for the command to complete. + // Defaults to 30 seconds if unset. + // +optional + Timeout *metav1.Duration `json:"timeout,omitempty"` } // DatastoreTLSSpec contains datastore TLS connections specifications. @@ -318,12 +340,12 @@ type CassandraConsistencySpec struct { // Values identical to gocql Consistency values. (defaults to LOCAL_QUORUM if not set). // +kubebuilder:validation:Enum=ANY;ONE;TWO;THREE;QUORUM;ALL;LOCAL_QUORUM;EACH_QUORUM;LOCAL_ONE // +optional - Consistency *gocql.Consistency `json:"consistency"` + Consistency *gocql.Consistency `json:"consistency,omitempty"` // SerialConsistency sets the consistency for the serial prtion of queries. Values identical to gocql SerialConsistency values. // (defaults to LOCAL_SERIAL if not set) // +kubebuilder:validation:Enum=SERIAL;LOCAL_SERIAL // +optional - SerialConsistency *gocql.SerialConsistency `json:"serialConsistency"` + SerialConsistency *gocql.SerialConsistency `json:"serialConsistency,omitempty"` } // CassandraSpec contains cassandra datastore connections specifications. @@ -344,7 +366,7 @@ type CassandraSpec struct { MaxConns int `json:"maxConns"` // ConnectTimeout is a timeout for initial dial to cassandra server. // +optional - ConnectTimeout *metav1.Duration `json:"connectTimeout"` + ConnectTimeout *metav1.Duration `json:"connectTimeout,omitempty"` // Consistency configuration. // +optional Consistency *CassandraConsistencySpec `json:"consistency,omitempty"` @@ -560,7 +582,7 @@ type TemporalUISpec struct { // Number of desired replicas for the ui. Default to 1. // +kubebuilder:validation:Minimum=1 // +optional - Replicas *int32 `json:"replicas"` + Replicas *int32 `json:"replicas,omitempty"` // Compute Resources required by the ui. // More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ // +optional @@ -667,23 +689,23 @@ type CertificatesDurationSpec struct { // RootCACertificate is the 'duration' (i.e. lifetime) of the Root CA Certificate. // It defaults to 10 years. // +optional - RootCACertificate *metav1.Duration `json:"rootCACertificate"` //nolint:tagliatelle + RootCACertificate *metav1.Duration `json:"rootCACertificate,omitempty"` //nolint:tagliatelle // IntermediateCACertificates is the 'duration' (i.e. lifetime) of the intermediate CAs Certificates. // It defaults to 5 years. // +optional - IntermediateCAsCertificates *metav1.Duration `json:"intermediateCAsCertificates"` + IntermediateCAsCertificates *metav1.Duration `json:"intermediateCAsCertificates,omitempty"` // ClientCertificates is the 'duration' (i.e. lifetime) of the client certificates. // It defaults to 1 year. // +optional - ClientCertificates *metav1.Duration `json:"clientCertificates"` + ClientCertificates *metav1.Duration `json:"clientCertificates,omitempty"` // FrontendCertificate is the 'duration' (i.e. lifetime) of the frontend certificate. // It defaults to 1 year. // +optional - FrontendCertificate *metav1.Duration `json:"frontendCertificate"` + FrontendCertificate *metav1.Duration `json:"frontendCertificate,omitempty"` // InternodeCertificate is the 'duration' (i.e. lifetime) of the internode certificate. // It defaults to 1 year. // +optional - InternodeCertificate *metav1.Duration `json:"internodeCertificate"` + InternodeCertificate *metav1.Duration `json:"internodeCertificate,omitempty"` } // MTLSSpec defines parameters for the temporal encryption in transit with mTLS. @@ -709,7 +731,7 @@ type MTLSSpec struct { // Defaults to 1 hour. // Useless if mTLS provider is not cert-manager. // +optional - RefreshInterval *metav1.Duration `json:"refreshInterval"` + RefreshInterval *metav1.Duration `json:"refreshInterval,omitempty"` // RenewBefore is defines how long before the currently issued certificate's expiry // cert-manager should renew the certificate. The default is 2/3 of the // issued certificate's duration. Minimum accepted value is 5 minutes. @@ -835,7 +857,7 @@ type DynamicConfigSpec struct { // PollInterval defines how often the config should be updated by checking provided values. // Defaults to 10s. // +optional - PollInterval *metav1.Duration `json:"pollInterval"` + PollInterval *metav1.Duration `json:"pollInterval,omitempty"` // Values contains all dynamic config keys and their constrained values. Values map[string][]ConstrainedValue `json:"values"` } @@ -958,12 +980,12 @@ type AuthorizationSpecJWTKeyProvider struct { // KeySourceURIs is a list of URIs where the JWT signing keys can be obtained. These URIs are used by the // authorization system to fetch the public keys necessary for validating JWT tokens. // +optional - KeySourceURIs []string `json:"keySourceURIs"` + KeySourceURIs []string `json:"keySourceURIs,omitempty"` // RefreshInterval defines the time interval at which temporal should refresh the JWT signing keys from // the specified URIs. // +optional - RefreshInterval *metav1.Duration `json:"refreshInterval"` + RefreshInterval *metav1.Duration `json:"refreshInterval,omitempty"` } // S3Archiver is the S3 archival provider configuration. @@ -1014,7 +1036,7 @@ type TemporalClusterSpec struct { // Version defines the temporal version the cluster to be deployed. // This version impacts the underlying persistence schemas versions. // +optional - Version *version.Version `json:"version"` + Version *version.Version `json:"version,omitempty"` // Log defines temporal cluster's logger configuration. // +optional Log *LogSpec `json:"log,omitempty"` @@ -1023,7 +1045,7 @@ type TemporalClusterSpec struct { // +optional //+kubebuilder:default:=300 //+kubebuilder:validation:Minimum=1 - JobTTLSecondsAfterFinished *int32 `json:"jobTtlSecondsAfterFinished"` + JobTTLSecondsAfterFinished *int32 `json:"jobTtlSecondsAfterFinished,omitempty"` // JobResources allows set resources for setup/update jobs. // +optional JobResources corev1.ResourceRequirements `json:"jobResources,omitempty"` @@ -1098,7 +1120,7 @@ type TemporalPersistenceStatus struct { VisibilityStore *DatastoreStatus `json:"visibilityStore"` // SecondaryVisibilityStore holds the secondary visibility datastore status. // +optional - SecondaryVisibilityStore *DatastoreStatus `json:"secondaryVisibilityStore"` + SecondaryVisibilityStore *DatastoreStatus `json:"secondaryVisibilityStore,omitempty"` // AdvancedVisibilityStore holds the advanced visibility datastore status. // +optional AdvancedVisibilityStore *DatastoreStatus `json:"advancedVisibilityStore,omitempty"` @@ -1193,6 +1215,17 @@ func (c *TemporalCluster) GetPublicClientAddress() string { return fmt.Sprintf("%s.%s:%d", c.ChildResourceName("frontend"), c.GetNamespace(), *c.Spec.Services.Frontend.Port) } +// GetOperatorClientAddress returns the address the operator uses for its own connections to the cluster. +// It always prefers the internal frontend when it's enabled, even when frontend mTLS is enabled: +// the internal frontend is served using the internode mTLS settings, so the frontend mTLS +// configuration doesn't apply to it. +func (c *TemporalCluster) GetOperatorClientAddress() string { + if c.Spec.Services != nil && c.Spec.Services.InternalFrontend.IsEnabled() { + return fmt.Sprintf("%s.%s:%d", c.ChildResourceName("internal-frontend-headless"), c.GetNamespace(), *c.Spec.Services.InternalFrontend.Port) + } + return c.GetPublicClientAddress() +} + // IsReady returns true if the TemporalCluster's conditions reports it ready. func (c *TemporalCluster) IsReady() bool { for _, condition := range c.Status.Conditions { diff --git a/api/v1beta1/temporalcluster_types_test.go b/api/v1beta1/temporalcluster_types_test.go new file mode 100644 index 00000000..f50d3973 --- /dev/null +++ b/api/v1beta1/temporalcluster_types_test.go @@ -0,0 +1,136 @@ +// Licensed to Alexandre VILAIN under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Alexandre VILAIN licenses this file to you 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_test + +import ( + "testing" + + "github.com/alexandrevilain/temporal-operator/api/v1beta1" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" +) + +func newFakeTemporalCluster(frontendMTLS, internodeMTLS, internalFrontend bool) *v1beta1.TemporalCluster { + cluster := &v1beta1.TemporalCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake", + Namespace: "default", + }, + Spec: v1beta1.TemporalClusterSpec{ + Services: &v1beta1.ServicesSpec{ + Frontend: &v1beta1.ServiceSpec{ + Port: ptr.To[int32](7233), + }, + }, + }, + } + + if internalFrontend { + cluster.Spec.Services.InternalFrontend = &v1beta1.InternalFrontendServiceSpec{ + ServiceSpec: v1beta1.ServiceSpec{ + Port: ptr.To[int32](7236), + }, + Enabled: true, + } + } + + if frontendMTLS || internodeMTLS { + cluster.Spec.MTLS = &v1beta1.MTLSSpec{ + Provider: v1beta1.CertManagerMTLSProvider, + } + if frontendMTLS { + cluster.Spec.MTLS.Frontend = &v1beta1.FrontendMTLSSpec{Enabled: true} + } + if internodeMTLS { + cluster.Spec.MTLS.Internode = &v1beta1.InternodeMTLSSpec{Enabled: true} + } + } + + return cluster +} + +func TestGetPublicClientAddressAndGetOperatorClientAddress(t *testing.T) { + frontendAddress := "fake-frontend.default:7233" + internalFrontendAddress := "fake-internal-frontend-headless.default:7236" + + tests := map[string]struct { + frontendMTLS bool + internodeMTLS bool + internalFrontend bool + expectedPublicAddress string + expectedOperatorAddress string + }{ + "no mTLS, no internal frontend": { + expectedPublicAddress: frontendAddress, + expectedOperatorAddress: frontendAddress, + }, + "frontend mTLS, no internal frontend": { + frontendMTLS: true, + expectedPublicAddress: frontendAddress, + expectedOperatorAddress: frontendAddress, + }, + "internode mTLS, no internal frontend": { + internodeMTLS: true, + expectedPublicAddress: frontendAddress, + expectedOperatorAddress: frontendAddress, + }, + "frontend and internode mTLS, no internal frontend": { + frontendMTLS: true, + internodeMTLS: true, + expectedPublicAddress: frontendAddress, + expectedOperatorAddress: frontendAddress, + }, + "no mTLS, internal frontend": { + internalFrontend: true, + expectedPublicAddress: internalFrontendAddress, + expectedOperatorAddress: internalFrontendAddress, + }, + "frontend mTLS, internal frontend": { + // GetPublicClientAddress always returns the public frontend address + // when frontend mTLS is enabled, while the operator uses the + // internal frontend for its own connections. + frontendMTLS: true, + internalFrontend: true, + expectedPublicAddress: frontendAddress, + expectedOperatorAddress: internalFrontendAddress, + }, + "internode mTLS, internal frontend": { + internodeMTLS: true, + internalFrontend: true, + expectedPublicAddress: internalFrontendAddress, + expectedOperatorAddress: internalFrontendAddress, + }, + "frontend and internode mTLS, internal frontend": { + frontendMTLS: true, + internodeMTLS: true, + internalFrontend: true, + expectedPublicAddress: frontendAddress, + expectedOperatorAddress: internalFrontendAddress, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + cluster := newFakeTemporalCluster(test.frontendMTLS, test.internodeMTLS, test.internalFrontend) + + assert.Equal(t, test.expectedPublicAddress, cluster.GetPublicClientAddress()) + assert.Equal(t, test.expectedOperatorAddress, cluster.GetOperatorClientAddress()) + }) + } +} diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 6f33f6af..c586faf7 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -930,6 +930,31 @@ func (in *S3Credentials) DeepCopy() *S3Credentials { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SQLPasswordCommandSpec) DeepCopyInto(out *SQLPasswordCommandSpec) { + *out = *in + if in.Args != nil { + in, out := &in.Args, &out.Args + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(metav1.Duration) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SQLPasswordCommandSpec. +func (in *SQLPasswordCommandSpec) DeepCopy() *SQLPasswordCommandSpec { + if in == nil { + return nil + } + out := new(SQLPasswordCommandSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SQLSpec) DeepCopyInto(out *SQLSpec) { *out = *in @@ -946,6 +971,11 @@ func (in *SQLSpec) DeepCopyInto(out *SQLSpec) { *out = new(string) **out = **in } + if in.PasswordCommand != nil { + in, out := &in.PasswordCommand, &out.PasswordCommand + *out = new(SQLPasswordCommandSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SQLSpec. diff --git a/charts/temporal-operator/crds/temporal-operator.crds.yaml b/charts/temporal-operator/crds/temporal-operator.crds.yaml index 158b07f5..5c849aba 100644 --- a/charts/temporal-operator/crds/temporal-operator.crds.yaml +++ b/charts/temporal-operator/crds/temporal-operator.crds.yaml @@ -2,7 +2,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.3 + controller-gen.kubebuilder.io/version: v0.21.0 name: temporalclusterclients.temporal.io spec: group: temporal.io @@ -90,7 +90,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.3 + controller-gen.kubebuilder.io/version: v0.21.0 name: temporalclusters.temporal.io spec: group: temporal.io @@ -156,6 +156,8 @@ spec: description: Override configuration for the temporal service Deployment. properties: + jsonPatch: + x-kubernetes-preserve-unknown-fields: true metadata: description: |- ObjectMetaOverride provides the ability to override an object metadata. @@ -222,7 +224,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -606,7 +608,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -769,6 +771,12 @@ spec: mTLS for network between cluster nodes. type: boolean type: object + permissiveMetrics: + description: |- + PermissiveMetrics allows insecure HTTP requests to the metrics endpoint. + This is handy if the metrics collector does not support mTLS. + Useless if mTLS provider is not istio + type: boolean provider: default: cert-manager description: Provider defines the tool used to manage mTLS certificates. @@ -974,6 +982,11 @@ spec: It requires Prometheus >= v2.28.0. pattern: (^0|([0-9]*[.])?[0-9]+((K|M|G|T|E|P)i?)?B)$ type: string + convertClassicHistogramsToNHCB: + description: |- + Whether to convert all scraped classic histograms into a native histogram with custom buckets. + It requires Prometheus >= v3.0.0. + type: boolean endpoints: description: |- List of endpoints part of this ServiceMonitor. @@ -1249,6 +1262,14 @@ spec: type: string type: object type: array + noProxy: + description: |- + `noProxy` is a comma-separated string that can contain IPs, CIDR notation, domain names + that should be excluded from proxying. IP and domain names can + contain port numbers. + + It requires Prometheus >= v2.43.0, Alertmanager >= v0.25.0 or Thanos >= v0.32.0. + type: string oauth2: description: |- `oauth2` configures the OAuth2 settings to use when scraping the target. @@ -1355,7 +1376,7 @@ spec: that should be excluded from proxying. IP and domain names can contain port numbers. - It requires Prometheus >= v2.43.0 or Alertmanager >= 0.25.0. + It requires Prometheus >= v2.43.0, Alertmanager >= v0.25.0 or Thanos >= v0.32.0. type: string proxyConnectHeader: additionalProperties: @@ -1391,19 +1412,19 @@ spec: ProxyConnectHeader optionally specifies headers to send to proxies during CONNECT requests. - It requires Prometheus >= v2.43.0 or Alertmanager >= 0.25.0. + It requires Prometheus >= v2.43.0, Alertmanager >= v0.25.0 or Thanos >= v0.32.0. type: object x-kubernetes-map-type: atomic proxyFromEnvironment: description: |- Whether to use the proxy configuration defined by environment variables (HTTP_PROXY, HTTPS_PROXY, and NO_PROXY). - It requires Prometheus >= v2.43.0 or Alertmanager >= 0.25.0. + It requires Prometheus >= v2.43.0, Alertmanager >= v0.25.0 or Thanos >= v0.32.0. type: boolean proxyUrl: description: '`proxyURL` defines the HTTP proxy server to use.' - pattern: ^http(s)?://.+$ + pattern: ^(http|https|socks5)://.+$ type: string scopes: description: '`scopes` defines the OAuth2 @@ -1567,7 +1588,7 @@ spec: description: |- Maximum acceptable TLS version. - It requires Prometheus >= v2.41.0. + It requires Prometheus >= v2.41.0 or Thanos >= v0.31.0. enum: - TLS10 - TLS11 @@ -1578,7 +1599,7 @@ spec: description: |- Minimum acceptable TLS version. - It requires Prometheus >= v2.35.0. + It requires Prometheus >= v2.35.0 or Thanos >= v0.28.0. enum: - TLS10 - TLS11 @@ -1620,10 +1641,52 @@ spec: It takes precedence over `targetPort`. type: string - proxyUrl: + proxyConnectHeader: + additionalProperties: + items: + description: SecretKeySelector selects + a key of a Secret. + properties: + key: + description: The key of the secret + to select from. Must be a valid + secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the + Secret or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: array + description: |- + ProxyConnectHeader optionally specifies headers to send to + proxies during CONNECT requests. + + It requires Prometheus >= v2.43.0, Alertmanager >= v0.25.0 or Thanos >= v0.32.0. + type: object + x-kubernetes-map-type: atomic + proxyFromEnvironment: description: |- - `proxyURL` configures the HTTP Proxy URL (e.g. - "http://proxyserver:2195") to go through when scraping the target. + Whether to use the proxy configuration defined by environment variables (HTTP_PROXY, HTTPS_PROXY, and NO_PROXY). + + It requires Prometheus >= v2.43.0, Alertmanager >= v0.25.0 or Thanos >= v0.32.0. + type: boolean + proxyUrl: + description: '`proxyURL` defines the HTTP + proxy server to use.' + pattern: ^(http|https|socks5)://.+$ type: string relabelings: description: |- @@ -1738,6 +1801,7 @@ spec: If empty, Prometheus uses the global scrape timeout unless it is less than the target's scrape interval value in which the latter is used. + The value cannot be greater than the scrape interval otherwise the operator will reject the resource. pattern: ^(0|(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?)$ type: string targetPort: @@ -1915,7 +1979,7 @@ spec: description: |- Maximum acceptable TLS version. - It requires Prometheus >= v2.41.0. + It requires Prometheus >= v2.41.0 or Thanos >= v0.31.0. enum: - TLS10 - TLS11 @@ -1926,7 +1990,7 @@ spec: description: |- Minimum acceptable TLS version. - It requires Prometheus >= v2.35.0. + It requires Prometheus >= v2.35.0 or Thanos >= v0.28.0. enum: - TLS10 - TLS11 @@ -1948,6 +2012,18 @@ spec: type: boolean type: object type: array + fallbackScrapeProtocol: + description: |- + The protocol to use if a scrape returns blank, unparseable, or otherwise invalid Content-Type. + + It requires Prometheus >= v3.0.0. + enum: + - PrometheusProto + - OpenMetricsText0.0.1 + - OpenMetricsText1.0.0 + - PrometheusText0.0.4 + - PrometheusText1.0.0 + type: string jobLabel: description: |- `jobLabel` selects the label from the associated Kubernetes `Service` @@ -2045,6 +2121,8 @@ spec: description: |- Whether to scrape a classic histogram that is also exposed as a native histogram. It requires Prometheus >= v2.45.0. + + Notice: `scrapeClassicHistograms` corresponds to the `always_scrape_classic_histograms` field in the Prometheus configuration. type: boolean scrapeProtocols: description: |- @@ -2062,11 +2140,13 @@ spec: * `OpenMetricsText1.0.0` * `PrometheusProto` * `PrometheusText0.0.4` + * `PrometheusText1.0.0` enum: - PrometheusProto - OpenMetricsText0.0.1 - OpenMetricsText1.0.0 - PrometheusText0.0.4 + - PrometheusText1.0.0 type: string type: array x-kubernetes-list-type: set @@ -2118,6 +2198,18 @@ spec: type: object type: object x-kubernetes-map-type: atomic + selectorMechanism: + description: |- + Mechanism used to select the endpoints to scrape. + By default, the selection process relies on relabel configurations to filter the discovered targets. + Alternatively, you can opt in for role selectors, which may offer better efficiency in large clusters. + Which strategy is best for your use case needs to be carefully evaluated. + + It requires Prometheus >= v2.17.0. + enum: + - RelabelConfig + - RoleSelector + type: string targetLabels: description: |- `targetLabels` defines the labels which are transferred from the @@ -2181,7 +2273,7 @@ spec: - LOCAL_QUORUM - EACH_QUORUM - LOCAL_ONE - type: integer + type: string serialConsistency: description: |- SerialConsistency sets the consistency for the serial prtion of queries. Values identical to gocql SerialConsistency values. @@ -2189,7 +2281,7 @@ spec: enum: - SERIAL - LOCAL_SERIAL - type: integer + type: string type: object datacenter: description: Datacenter is the data center filter arg @@ -2343,6 +2435,33 @@ spec: description: MaxIdleConns is the max number of idle connections to this datastore. type: integer + passwordCommand: + description: |- + PasswordCommand executes an external command whose standard output is used + as the datastore password, for instance to generate a short-lived cloud IAM + auth token (AWS RDS, GCP Cloud SQL). Mutually exclusive with the datastore + passwordSecretRef. When the command returns an expiring token, set + maxConnLifetime so connections are recycled before the token expires. + Requires Temporal >= 1.31.0. + properties: + args: + description: Args is the list of arguments passed + to the command. + items: + type: string + type: array + command: + description: Command is the path to the executable + to run. + type: string + timeout: + description: |- + Timeout is the maximum duration to wait for the command to complete. + Defaults to 30 seconds if unset. + type: string + required: + - command + type: object pluginName: description: PluginName is the name of SQL plugin. enum: @@ -2452,7 +2571,7 @@ spec: - LOCAL_QUORUM - EACH_QUORUM - LOCAL_ONE - type: integer + type: string serialConsistency: description: |- SerialConsistency sets the consistency for the serial prtion of queries. Values identical to gocql SerialConsistency values. @@ -2460,7 +2579,7 @@ spec: enum: - SERIAL - LOCAL_SERIAL - type: integer + type: string type: object datacenter: description: Datacenter is the data center filter arg @@ -2614,6 +2733,33 @@ spec: description: MaxIdleConns is the max number of idle connections to this datastore. type: integer + passwordCommand: + description: |- + PasswordCommand executes an external command whose standard output is used + as the datastore password, for instance to generate a short-lived cloud IAM + auth token (AWS RDS, GCP Cloud SQL). Mutually exclusive with the datastore + passwordSecretRef. When the command returns an expiring token, set + maxConnLifetime so connections are recycled before the token expires. + Requires Temporal >= 1.31.0. + properties: + args: + description: Args is the list of arguments passed + to the command. + items: + type: string + type: array + command: + description: Command is the path to the executable + to run. + type: string + timeout: + description: |- + Timeout is the maximum duration to wait for the command to complete. + Defaults to 30 seconds if unset. + type: string + required: + - command + type: object pluginName: description: PluginName is the name of SQL plugin. enum: @@ -2725,7 +2871,7 @@ spec: - LOCAL_QUORUM - EACH_QUORUM - LOCAL_ONE - type: integer + type: string serialConsistency: description: |- SerialConsistency sets the consistency for the serial prtion of queries. Values identical to gocql SerialConsistency values. @@ -2733,7 +2879,7 @@ spec: enum: - SERIAL - LOCAL_SERIAL - type: integer + type: string type: object datacenter: description: Datacenter is the data center filter arg @@ -2887,6 +3033,33 @@ spec: description: MaxIdleConns is the max number of idle connections to this datastore. type: integer + passwordCommand: + description: |- + PasswordCommand executes an external command whose standard output is used + as the datastore password, for instance to generate a short-lived cloud IAM + auth token (AWS RDS, GCP Cloud SQL). Mutually exclusive with the datastore + passwordSecretRef. When the command returns an expiring token, set + maxConnLifetime so connections are recycled before the token expires. + Requires Temporal >= 1.31.0. + properties: + args: + description: Args is the list of arguments passed + to the command. + items: + type: string + type: array + command: + description: Command is the path to the executable + to run. + type: string + timeout: + description: |- + Timeout is the maximum duration to wait for the command to complete. + Defaults to 30 seconds if unset. + type: string + required: + - command + type: object pluginName: description: PluginName is the name of SQL plugin. enum: @@ -2996,7 +3169,7 @@ spec: - LOCAL_QUORUM - EACH_QUORUM - LOCAL_ONE - type: integer + type: string serialConsistency: description: |- SerialConsistency sets the consistency for the serial prtion of queries. Values identical to gocql SerialConsistency values. @@ -3004,7 +3177,7 @@ spec: enum: - SERIAL - LOCAL_SERIAL - type: integer + type: string type: object datacenter: description: Datacenter is the data center filter arg @@ -3158,6 +3331,33 @@ spec: description: MaxIdleConns is the max number of idle connections to this datastore. type: integer + passwordCommand: + description: |- + PasswordCommand executes an external command whose standard output is used + as the datastore password, for instance to generate a short-lived cloud IAM + auth token (AWS RDS, GCP Cloud SQL). Mutually exclusive with the datastore + passwordSecretRef. When the command returns an expiring token, set + maxConnLifetime so connections are recycled before the token expires. + Requires Temporal >= 1.31.0. + properties: + args: + description: Args is the list of arguments passed + to the command. + items: + type: string + type: array + command: + description: Command is the path to the executable + to run. + type: string + timeout: + description: |- + Timeout is the maximum duration to wait for the command to complete. + Defaults to 30 seconds if unset. + type: string + required: + - command + type: object pluginName: description: PluginName is the name of SQL plugin. enum: @@ -3284,6 +3484,8 @@ spec: description: Override configuration for the temporal service Deployment. properties: + jsonPatch: + x-kubernetes-preserve-unknown-fields: true metadata: description: |- ObjectMetaOverride provides the ability to override an object metadata. @@ -3366,7 +3568,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -3456,6 +3658,8 @@ spec: description: Override configuration for the temporal service Deployment. properties: + jsonPatch: + x-kubernetes-preserve-unknown-fields: true metadata: description: |- ObjectMetaOverride provides the ability to override an object metadata. @@ -3538,7 +3742,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -3635,6 +3839,8 @@ spec: description: Override configuration for the temporal service Deployment. properties: + jsonPatch: + x-kubernetes-preserve-unknown-fields: true metadata: description: |- ObjectMetaOverride provides the ability to override an object metadata. @@ -3717,7 +3923,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -3807,6 +4013,8 @@ spec: description: Override configuration for the temporal service Deployment. properties: + jsonPatch: + x-kubernetes-preserve-unknown-fields: true metadata: description: |- ObjectMetaOverride provides the ability to override an object metadata. @@ -3889,7 +4097,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -3950,6 +4158,8 @@ spec: description: Override configuration for the temporal service Deployment. properties: + jsonPatch: + x-kubernetes-preserve-unknown-fields: true metadata: description: |- ObjectMetaOverride provides the ability to override an object metadata. @@ -4044,6 +4254,8 @@ spec: description: Override configuration for the temporal service Deployment. properties: + jsonPatch: + x-kubernetes-preserve-unknown-fields: true metadata: description: |- ObjectMetaOverride provides the ability to override an object metadata. @@ -4126,7 +4338,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -4248,6 +4460,8 @@ spec: description: Override configuration for the temporal service Deployment. properties: + jsonPatch: + x-kubernetes-preserve-unknown-fields: true metadata: description: |- ObjectMetaOverride provides the ability to override an object metadata. @@ -4320,7 +4534,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -4590,7 +4804,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.3 + controller-gen.kubebuilder.io/version: v0.21.0 name: temporalnamespaces.temporal.io spec: group: temporal.io @@ -4637,6 +4851,11 @@ spec: AllowDeletion makes the controller delete the Temporal namespace if the CRD is deleted. type: boolean + allowSearchAttributeDeletion: + description: |- + AllowSearchAttributeDeletion makes the controller remove custom search attributes + from the Temporal server if they are not present in the spec. + type: boolean archival: description: |- Archival is a per-namespace archival configuration. @@ -4717,6 +4936,13 @@ spec: items: type: string type: array + customSearchAttributes: + additionalProperties: + type: string + description: |- + CustomSearchAttributes is an optional mapping of custom search attribute names to types. + Supported types: Text, Keyword, Int, Double, Bool, DateTime, KeywordList. + type: object data: additionalProperties: type: string @@ -4815,7 +5041,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.3 + controller-gen.kubebuilder.io/version: v0.21.0 name: temporalschedules.temporal.io spec: group: temporal.io diff --git a/config/crd/bases/temporal.io_temporalclusterclients.yaml b/config/crd/bases/temporal.io_temporalclusterclients.yaml index 9bf7a7fd..ba4b5a25 100644 --- a/config/crd/bases/temporal.io_temporalclusterclients.yaml +++ b/config/crd/bases/temporal.io_temporalclusterclients.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.3 + controller-gen.kubebuilder.io/version: v0.21.0 name: temporalclusterclients.temporal.io spec: group: temporal.io diff --git a/config/crd/bases/temporal.io_temporalclusters.yaml b/config/crd/bases/temporal.io_temporalclusters.yaml index 7eebc60f..f9a9bf19 100644 --- a/config/crd/bases/temporal.io_temporalclusters.yaml +++ b/config/crd/bases/temporal.io_temporalclusters.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.3 + controller-gen.kubebuilder.io/version: v0.21.0 name: temporalclusters.temporal.io spec: group: temporal.io @@ -129,7 +129,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -486,7 +486,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -2029,7 +2029,7 @@ spec: - LOCAL_QUORUM - EACH_QUORUM - LOCAL_ONE - type: integer + type: string serialConsistency: description: |- SerialConsistency sets the consistency for the serial prtion of queries. Values identical to gocql SerialConsistency values. @@ -2037,7 +2037,7 @@ spec: enum: - SERIAL - LOCAL_SERIAL - type: integer + type: string type: object datacenter: description: Datacenter is the data center filter arg for cassandra. @@ -2166,6 +2166,31 @@ spec: maxIdleConns: description: MaxIdleConns is the max number of idle connections to this datastore. type: integer + passwordCommand: + description: |- + PasswordCommand executes an external command whose standard output is used + as the datastore password, for instance to generate a short-lived cloud IAM + auth token (AWS RDS, GCP Cloud SQL). Mutually exclusive with the datastore + passwordSecretRef. When the command returns an expiring token, set + maxConnLifetime so connections are recycled before the token expires. + Requires Temporal >= 1.31.0. + properties: + args: + description: Args is the list of arguments passed to the command. + items: + type: string + type: array + command: + description: Command is the path to the executable to run. + type: string + timeout: + description: |- + Timeout is the maximum duration to wait for the command to complete. + Defaults to 30 seconds if unset. + type: string + required: + - command + type: object pluginName: description: PluginName is the name of SQL plugin. enum: @@ -2267,7 +2292,7 @@ spec: - LOCAL_QUORUM - EACH_QUORUM - LOCAL_ONE - type: integer + type: string serialConsistency: description: |- SerialConsistency sets the consistency for the serial prtion of queries. Values identical to gocql SerialConsistency values. @@ -2275,7 +2300,7 @@ spec: enum: - SERIAL - LOCAL_SERIAL - type: integer + type: string type: object datacenter: description: Datacenter is the data center filter arg for cassandra. @@ -2404,6 +2429,31 @@ spec: maxIdleConns: description: MaxIdleConns is the max number of idle connections to this datastore. type: integer + passwordCommand: + description: |- + PasswordCommand executes an external command whose standard output is used + as the datastore password, for instance to generate a short-lived cloud IAM + auth token (AWS RDS, GCP Cloud SQL). Mutually exclusive with the datastore + passwordSecretRef. When the command returns an expiring token, set + maxConnLifetime so connections are recycled before the token expires. + Requires Temporal >= 1.31.0. + properties: + args: + description: Args is the list of arguments passed to the command. + items: + type: string + type: array + command: + description: Command is the path to the executable to run. + type: string + timeout: + description: |- + Timeout is the maximum duration to wait for the command to complete. + Defaults to 30 seconds if unset. + type: string + required: + - command + type: object pluginName: description: PluginName is the name of SQL plugin. enum: @@ -2507,7 +2557,7 @@ spec: - LOCAL_QUORUM - EACH_QUORUM - LOCAL_ONE - type: integer + type: string serialConsistency: description: |- SerialConsistency sets the consistency for the serial prtion of queries. Values identical to gocql SerialConsistency values. @@ -2515,7 +2565,7 @@ spec: enum: - SERIAL - LOCAL_SERIAL - type: integer + type: string type: object datacenter: description: Datacenter is the data center filter arg for cassandra. @@ -2644,6 +2694,31 @@ spec: maxIdleConns: description: MaxIdleConns is the max number of idle connections to this datastore. type: integer + passwordCommand: + description: |- + PasswordCommand executes an external command whose standard output is used + as the datastore password, for instance to generate a short-lived cloud IAM + auth token (AWS RDS, GCP Cloud SQL). Mutually exclusive with the datastore + passwordSecretRef. When the command returns an expiring token, set + maxConnLifetime so connections are recycled before the token expires. + Requires Temporal >= 1.31.0. + properties: + args: + description: Args is the list of arguments passed to the command. + items: + type: string + type: array + command: + description: Command is the path to the executable to run. + type: string + timeout: + description: |- + Timeout is the maximum duration to wait for the command to complete. + Defaults to 30 seconds if unset. + type: string + required: + - command + type: object pluginName: description: PluginName is the name of SQL plugin. enum: @@ -2745,7 +2820,7 @@ spec: - LOCAL_QUORUM - EACH_QUORUM - LOCAL_ONE - type: integer + type: string serialConsistency: description: |- SerialConsistency sets the consistency for the serial prtion of queries. Values identical to gocql SerialConsistency values. @@ -2753,7 +2828,7 @@ spec: enum: - SERIAL - LOCAL_SERIAL - type: integer + type: string type: object datacenter: description: Datacenter is the data center filter arg for cassandra. @@ -2882,6 +2957,31 @@ spec: maxIdleConns: description: MaxIdleConns is the max number of idle connections to this datastore. type: integer + passwordCommand: + description: |- + PasswordCommand executes an external command whose standard output is used + as the datastore password, for instance to generate a short-lived cloud IAM + auth token (AWS RDS, GCP Cloud SQL). Mutually exclusive with the datastore + passwordSecretRef. When the command returns an expiring token, set + maxConnLifetime so connections are recycled before the token expires. + Requires Temporal >= 1.31.0. + properties: + args: + description: Args is the list of arguments passed to the command. + items: + type: string + type: array + command: + description: Command is the path to the executable to run. + type: string + timeout: + description: |- + Timeout is the maximum duration to wait for the command to complete. + Defaults to 30 seconds if unset. + type: string + required: + - command + type: object pluginName: description: PluginName is the name of SQL plugin. enum: @@ -3077,7 +3177,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -3244,7 +3344,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -3417,7 +3517,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -3584,7 +3684,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -3814,7 +3914,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -3998,7 +4098,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. diff --git a/config/crd/bases/temporal.io_temporalnamespaces.yaml b/config/crd/bases/temporal.io_temporalnamespaces.yaml index 66bee4b3..5c513141 100644 --- a/config/crd/bases/temporal.io_temporalnamespaces.yaml +++ b/config/crd/bases/temporal.io_temporalnamespaces.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.3 + controller-gen.kubebuilder.io/version: v0.21.0 name: temporalnamespaces.temporal.io spec: group: temporal.io diff --git a/config/crd/bases/temporal.io_temporalschedules.yaml b/config/crd/bases/temporal.io_temporalschedules.yaml index 7d0f4959..117f3ea6 100644 --- a/config/crd/bases/temporal.io_temporalschedules.yaml +++ b/config/crd/bases/temporal.io_temporalschedules.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.3 + controller-gen.kubebuilder.io/version: v0.21.0 name: temporalschedules.temporal.io spec: group: temporal.io diff --git a/docs/api/v1beta1.md b/docs/api/v1beta1.md index cec32b63..14ca8f4c 100644 --- a/docs/api/v1beta1.md +++ b/docs/api/v1beta1.md @@ -2721,6 +2721,66 @@ Kubernetes core/v1.SecretKeySelector +

SQLPasswordCommandSpec +

+

+(Appears on: +SQLSpec) +

+

SQLPasswordCommandSpec configures an external command used to retrieve the +datastore password at runtime. Available for Temporal clusters >= 1.31.0.

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+command
+ +string + +
+

Command is the path to the executable to run.

+
+args
+ +[]string + +
+(Optional) +

Args is the list of arguments passed to the command.

+
+timeout
+ + +Kubernetes meta/v1.Duration + + +
+(Optional) +

Timeout is the maximum duration to wait for the command to complete. +Defaults to 30 seconds if unset.

+
+
+

SQLSpec

@@ -2868,6 +2928,25 @@ string

GCPServiceAccount is the service account to use to authenticate with GCP CloudSQL.

+ + +passwordCommand
+ + +SQLPasswordCommandSpec + + + + +(Optional) +

PasswordCommand executes an external command whose standard output is used +as the datastore password, for instance to generate a short-lived cloud IAM +auth token (AWS RDS, GCP Cloud SQL). Mutually exclusive with the datastore +passwordSecretRef. When the command returns an expiring token, set +maxConnLifetime so connections are recycled before the token expires. +Requires Temporal >= 1.31.0.

+ + @@ -5297,6 +5376,32 @@ TemporalNamespaceArchivalSpec If not set, the default cluster configuration is used.

+ + +customSearchAttributes
+ +map[string]string + + + +(Optional) +

CustomSearchAttributes is an optional mapping of custom search attribute names to types. +Supported types: Text, Keyword, Int, Double, Bool, DateTime, KeywordList.

+ + + + +allowSearchAttributeDeletion
+ +bool + + + +(Optional) +

AllowSearchAttributeDeletion makes the controller remove custom search attributes +from the Temporal server if they are not present in the spec.

+ + @@ -5521,6 +5626,32 @@ TemporalNamespaceArchivalSpec If not set, the default cluster configuration is used.

+ + +customSearchAttributes
+ +map[string]string + + + +(Optional) +

CustomSearchAttributes is an optional mapping of custom search attribute names to types. +Supported types: Text, Keyword, Int, Double, Bool, DateTime, KeywordList.

+ + + + +allowSearchAttributeDeletion
+ +bool + + + +(Optional) +

AllowSearchAttributeDeletion makes the controller remove custom search attributes +from the Temporal server if they are not present in the spec.

+ + diff --git a/docs/features/mtls/cert-manager.md b/docs/features/mtls/cert-manager.md index 9deec99c..51a912b2 100644 --- a/docs/features/mtls/cert-manager.md +++ b/docs/features/mtls/cert-manager.md @@ -24,3 +24,12 @@ Here is a diagram of cert-manager's resources created by the operator and their ![diagram](/assets/mtls-certmanager.png) +## Operator connections + +The operator creates its own client connections to the cluster, for instance to reconcile `TemporalNamespace` or `TemporalSchedule` resources. The address and certificate it uses depend on the cluster configuration: + +- When the internal frontend is enabled (`spec.services.internalFrontend.enabled: true`), the operator connects to the internal frontend. The internal frontend is served using the internode mTLS settings, so the operator authenticates using the internode certificate when internode mTLS is enabled, and connects without TLS otherwise — even if frontend mTLS is enabled. +- Otherwise, the operator connects to the public frontend, using the frontend certificate when frontend mTLS is enabled. + +When [authorization](https://docs.temporal.io/self-hosted-guide/security#authorization) is configured on the cluster, prefer enabling the internal frontend: temporal applies the noop claim mapper and authorizer to connections going through the internal frontend, so the operator doesn't need any grant in your authorizer. Enabling internode mTLS alongside the internal frontend is recommended, otherwise the operator's traffic to the internal frontend is unencrypted. + diff --git a/go.mod b/go.mod index eeaefa34..01fd23b0 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,9 @@ module github.com/alexandrevilain/temporal-operator -go 1.24.5 - -toolchain go1.24.6 +go 1.26.4 require ( - github.com/Masterminds/semver/v3 v3.3.0 + github.com/Masterminds/semver/v3 v3.4.0 github.com/alexandrevilain/controller-tools v0.3.0 github.com/cert-manager/cert-manager v1.16.3 github.com/elliotchance/orderedmap/v2 v2.4.0 @@ -15,60 +13,80 @@ require ( github.com/google/uuid v1.6.0 github.com/gosimple/slug v1.14.0 github.com/lithammer/dedent v1.1.0 - github.com/onsi/ginkgo/v2 v2.22.0 - github.com/onsi/gomega v1.36.1 + github.com/onsi/ginkgo/v2 v2.27.2 + github.com/onsi/gomega v1.38.2 github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.85.0 - github.com/stretchr/testify v1.10.0 - go.temporal.io/api v1.52.0 - go.temporal.io/sdk v1.35.0 - go.temporal.io/server v1.28.1 - golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa - google.golang.org/protobuf v1.36.5 + github.com/stretchr/testify v1.11.1 + go.temporal.io/api v1.62.8 + go.temporal.io/sdk v1.41.1 + go.temporal.io/server v1.31.1 + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 + google.golang.org/grpc v1.79.3 + google.golang.org/protobuf v1.36.10 gopkg.in/yaml.v3 v3.0.1 istio.io/api v1.24.1 istio.io/client-go v1.24.0 - k8s.io/api v0.33.3 - k8s.io/apiextensions-apiserver v0.33.3 - k8s.io/apimachinery v0.33.3 - k8s.io/client-go v0.33.3 + k8s.io/api v0.35.1 + k8s.io/apiextensions-apiserver v0.35.0 + k8s.io/apimachinery v0.35.1 + k8s.io/client-go v0.35.1 k8s.io/klog/v2 v2.130.1 - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 - sigs.k8s.io/controller-runtime v0.21.0 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 + sigs.k8s.io/controller-runtime v0.23.3 sigs.k8s.io/e2e-framework v0.5.0 ) require ( - cel.dev/expr v0.20.0 // indirect - cloud.google.com/go v0.118.3 // indirect - cloud.google.com/go/auth v0.15.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect - cloud.google.com/go/compute/metadata v0.6.0 // indirect - cloud.google.com/go/iam v1.4.2 // indirect - cloud.google.com/go/monitoring v1.24.1 // indirect - cloud.google.com/go/storage v1.51.0 // indirect + cel.dev/expr v0.25.1 // indirect + cloud.google.com/go v0.121.6 // indirect + cloud.google.com/go/auth v0.17.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/iam v1.5.3 // indirect + cloud.google.com/go/monitoring v1.24.2 // indirect + cloud.google.com/go/storage v1.56.0 // indirect dario.cat/mergo v1.0.1 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect - github.com/aws/aws-sdk-go v1.55.6 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.13 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.13 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.98.0 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.14 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect + github.com/aws/smithy-go v1.24.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cactus/go-statsd-client/v5 v5.1.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect + github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect - github.com/emicklei/go-restful/v3 v3.12.1 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect - github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/fxamacker/cbor/v2 v2.8.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -80,20 +98,19 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/pprof v0.0.0-20250208200701-d0013a598941 // indirect + github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.5 // indirect - github.com/googleapis/gax-go/v2 v2.14.1 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/gosimple/unidecode v1.0.1 // indirect - github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect github.com/jmoiron/sqlx v1.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -103,26 +120,26 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/spdystream v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect - github.com/nexus-rpc/sdk-go v0.3.0 // indirect + github.com/nexus-rpc/sdk-go v0.6.0 // indirect github.com/olivere/elastic/v7 v7.0.32 // indirect - github.com/pborman/uuid v1.2.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.22.0 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/robfig/cron v1.2.0 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/cast v1.7.0 // indirect - github.com/spf13/cobra v1.8.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/cobra v1.10.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/temporalio/sqlparser v0.0.0-20231115171017-f4060bcfa6cb // indirect github.com/twmb/murmur3 v1.1.8 // indirect @@ -130,51 +147,52 @@ require ( github.com/urfave/cli v1.22.16 // indirect github.com/vladimirvivien/gexe v0.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect - go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect go.opentelemetry.io/otel/exporters/prometheus v0.56.0 // indirect - go.opentelemetry.io/otel/metric v1.34.0 // indirect - go.opentelemetry.io/otel/sdk v1.34.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect - go.opentelemetry.io/otel/trace v1.34.0 // indirect - go.opentelemetry.io/proto/otlp v1.5.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.uber.org/atomic v1.11.0 // indirect - go.uber.org/dig v1.18.0 // indirect - go.uber.org/fx v1.23.0 // indirect - go.uber.org/mock v0.5.0 // indirect + go.uber.org/dig v1.19.0 // indirect + go.uber.org/fx v1.24.0 // indirect + go.uber.org/mock v0.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/crypto v0.39.0 // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/oauth2 v0.28.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.27.0 // indirect - golang.org/x/time v0.10.0 // indirect - golang.org/x/tools v0.34.0 // indirect + go.uber.org/zap v1.27.1 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/term v0.43.0 // indirect + golang.org/x/text v0.37.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.44.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/api v0.224.0 // indirect - google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/grpc v1.71.0 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + google.golang.org/api v0.256.0 // indirect + google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/validator.v2 v2.0.1 // indirect - k8s.io/component-base v0.33.3 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/component-base v0.35.0 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect sigs.k8s.io/cli-utils v0.35.0 // indirect sigs.k8s.io/gateway-api v1.1.0 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect - sigs.k8s.io/yaml v1.5.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 041b3692..8738b026 100644 --- a/go.sum +++ b/go.sum @@ -1,53 +1,87 @@ -cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI= -cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.118.3 h1:jsypSnrE/w4mJysioGdMBg4MiW/hHx/sArFpaBWHdME= -cloud.google.com/go v0.118.3/go.mod h1:Lhs3YLnBlwJ4KA6nuObNMZ/fCbOQBPuWKPoE0Wa/9Vc= -cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= -cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= -cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= -cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= -cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= -cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= -cloud.google.com/go/iam v1.4.2 h1:4AckGYAYsowXeHzsn/LCKWIwSWLkdb0eGjH8wWkd27Q= -cloud.google.com/go/iam v1.4.2/go.mod h1:REGlrt8vSlh4dfCJfSEcNjLGq75wW75c5aU3FLOYq34= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= +cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI= +cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= +cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= +cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= -cloud.google.com/go/longrunning v0.6.5 h1:sD+t8DO8j4HKW4QfouCklg7ZC1qC4uzVZt8iz3uTW+Q= -cloud.google.com/go/longrunning v0.6.5/go.mod h1:Et04XK+0TTLKa5IPYryKf5DkpwImy6TluQ1QTLwlKmY= -cloud.google.com/go/monitoring v1.24.1 h1:vKiypZVFD/5a3BbQMvI4gZdl8445ITzXFh257XBgrS0= -cloud.google.com/go/monitoring v1.24.1/go.mod h1:Z05d1/vn9NaujqY2voG6pVQXoJGbp+r3laV+LySt9K0= -cloud.google.com/go/storage v1.51.0 h1:ZVZ11zCiD7b3k+cH5lQs/qcNaoSz3U9I0jgwVzqDlCw= -cloud.google.com/go/storage v1.51.0/go.mod h1:YEJfu/Ki3i5oHC/7jyTgsGZwdQ8P9hqMqvpi5kRKGgc= -cloud.google.com/go/trace v1.11.3 h1:c+I4YFjxRQjvAhRmSsmjpASUKq88chOX854ied0K/pE= -cloud.google.com/go/trace v1.11.3/go.mod h1:pt7zCYiDSQjC9Y2oqCsh9jF4GStB/hmjrYLsxRR27q8= +cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E= +cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY= +cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= +cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= +cloud.google.com/go/storage v1.56.0 h1:iixmq2Fse2tqxMbWhLWC9HfBj1qdxqAmiK8/eqtsLxI= +cloud.google.com/go/storage v1.56.0/go.mod h1:Tpuj6t4NweCLzlNbw9Z9iwxEkrSem20AetIeH/shgVU= +cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= +cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= +filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/alexandrevilain/controller-tools v0.3.0 h1:tdTQo9ivc53GKOkjAVDLB7uBA8JxUgoUNl4LSkF4HH8= github.com/alexandrevilain/controller-tools v0.3.0/go.mod h1:KHokLdmgMzRGkd46oPj8puNipJCArwJWnRApAaY0eM4= 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/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= -github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= +github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= +github.com/aws/aws-sdk-go-v2/config v1.32.13 h1:5KgbxMaS2coSWRrx9TX/QtWbqzgQkOdEa3sZPhBhCSg= +github.com/aws/aws-sdk-go-v2/config v1.32.13/go.mod h1:8zz7wedqtCbw5e9Mi2doEwDyEgHcEE9YOJp6a8jdSMY= +github.com/aws/aws-sdk-go-v2/credentials v1.19.13 h1:mA59E3fokBvyEGHKFdnpNNrvaR351cqiHgRg+JzOSRI= +github.com/aws/aws-sdk-go-v2/credentials v1.19.13/go.mod h1:yoTXOQKea18nrM69wGF9jBdG4WocSZA1h38A+t/MAsk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeDLaS3bmHD0YndtA6UP884g= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21/go.mod h1:YWNWJQNjKigKY1RHVJCuupeWDrrHjRqHm0N9rdrWzYI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ= +github.com/aws/aws-sdk-go-v2/service/s3 v1.98.0 h1:foqo/ocQ7WqKwy3FojGtZQJo0FR4vto9qnz9VaumbCo= +github.com/aws/aws-sdk-go-v2/service/s3 v1.98.0/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.9/go.mod h1:7yuQJoT+OoH8aqIxw9vwF+8KpvLZ8AWmvmUWHsGQZvI= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.14 h1:GcLE9ba5ehAQma6wlopUesYg/hbcOhFNWTjELkiWkh4= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.14/go.mod h1:WSvS1NLr7JaPunCXqpJnWk1Bjo7IxzZXrZi1QQCkuqM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18 h1:mP49nTpfKtpXLt5SLn8Uv8z6W+03jYVoOSAl/c02nog= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18/go.mod h1:YO8TrYtFdl5w/4vmjL8zaBSsiNp3w0L1FfKVKenZT7w= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBUdErbMnAFFp12Lm/U= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw= +github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= +github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= @@ -60,41 +94,36 @@ github.com/cactus/go-statsd-client/v5 v5.1.0 h1:sbbdfIl9PgisjEoXzvXI1lwUKWElngsj github.com/cactus/go-statsd-client/v5 v5.1.0/go.mod h1:COEvJ1E+/E2L4q6QE5CkjWPi4eeDw9maJBMIuMPBZbY= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cert-manager/cert-manager v1.16.3 h1:seEF5eidFaeduaCuM85PFEuzH/1X/HOV5Y8zDQrHgpc= github.com/cert-manager/cert-manager v1.16.3/go.mod h1:6JQ/GAZ6dH+erqS1BbaqorPy8idJzCtWFUmJQBTjo6Q= 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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= -github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 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/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elliotchance/orderedmap/v2 v2.4.0 h1:6tUmMwD9F998FNpwFxA5E6NQvSpk2PVw7RKsVq3+2Cw= github.com/elliotchance/orderedmap/v2 v2.4.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q= -github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= -github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= -github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= -github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= -github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= +github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= +github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= -github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= +github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= @@ -107,16 +136,20 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= -github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= +github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/go-faker/faker/v4 v4.6.0 h1:6aOPzNptRiDwD14HuAnEtlTa+D1IfFuEHO8+vEFwjTs= github.com/go-faker/faker/v4 v4.6.0/go.mod h1:ZmrHuVtTTm2Em9e0Du6CJ9CADaLEzGXW62z1YqFH0m0= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= 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= @@ -133,22 +166,18 @@ github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo= github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gocql/gocql v1.7.0 h1:O+7U7/1gSN7QTEAaMEsJc1Oq2QHXvCWoF3DFK9HDHus= github.com/gocql/gocql v1.7.0/go.mod h1:vnlvXyFZeLBF0Wy+RS8hrOdbn0UWsWtdg07XJnFxZ+4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -156,10 +185,8 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= 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= @@ -167,27 +194,26 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= -github.com/google/pprof v0.0.0-20250208200701-d0013a598941 h1:43XjGa6toxLpeksjcxs1jIoIyr+vUfOqY2c6HB4bpoc= -github.com/google/pprof v0.0.0-20250208200701-d0013a598941/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/googleapis/enterprise-certificate-proxy v0.3.5 h1:VgzTY2jogw3xt39CusEnFJWm7rlsq5yL5q9XdLOuP5g= -github.com/googleapis/enterprise-certificate-proxy v0.3.5/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= -github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/gosimple/slug v1.14.0 h1:RtTL/71mJNDfpUbCOmnf/XFkzKRtD6wL6Uy+3akm4Es= github.com/gosimple/slug v1.14.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= -github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= -github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= @@ -196,22 +222,27 @@ github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSAS github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= -github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= 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/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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 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= @@ -227,8 +258,14 @@ github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffkt github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -240,24 +277,23 @@ github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVO 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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/nexus-rpc/sdk-go v0.3.0 h1:Y3B0kLYbMhd4C2u00kcYajvmOrfozEtTV/nHSnV57jA= -github.com/nexus-rpc/sdk-go v0.3.0/go.mod h1:TpfkM2Cw0Rlk9drGkoiSMpFqflKTiQLWUNyKJjF8mKQ= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nexus-rpc/sdk-go v0.6.0 h1:QRgnP2zTbxEbiyWG/aXH8uSC5LV/Mg1fqb19jb4DBlo= +github.com/nexus-rpc/sdk-go v0.6.0/go.mod h1:FHdPfVQwRuJFZFTF0Y2GOAxCrbIBNrcPna9slkGKPYk= github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E= github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k= -github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= -github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= -github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= -github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= @@ -267,50 +303,57 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.85.0 h1:oY+F5FZFmCjCyzkHWPjVQpzvnvEB/0FP+iyzDUUlqFc= github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.85.0/go.mod h1:VB7wtBmDT6W2RJHzsvPZlBId+EnmeQA0d33fFTXvraM= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +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 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.10.0 h1:a5/WeUlSDCvV5a45ljW2ZFtV0bTDpkfSAj3uqB6Sc+0= +github.com/spf13/cobra v1.10.0/go.mod h1:9dhySC7dnTtEiqzmqfkLj47BslqLCUPMXjG2lj/NgoE= +github.com/spf13/pflag v1.0.8/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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/temporalio/sqlparser v0.0.0-20231115171017-f4060bcfa6cb h1:YzHH/U/dN7vMP+glybzcXRTczTrgfdRisNTzAj7La04= github.com/temporalio/sqlparser v0.0.0-20231115171017-f4060bcfa6cb/go.mod h1:143qKdh3G45IgV9p+gbAwp3ikRDI8mxsijFiXDfuxsw= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= github.com/uber-go/tally/v4 v4.1.17 h1:C+U4BKtVDXTszuzU+WH8JVQvRVnaVKxzZrROFyDrvS8= @@ -324,16 +367,16 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= -go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +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/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= +go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +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/otlpmetric/otlpmetricgrpc v1.34.0 h1:ajl4QczuJVA2TU9W9AGw++86Xga/RKt//16z/yxPgdk= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0/go.mod h1:Vn3/rlOJ3ntf/Q3zAI0V5lDnTbHGaUsNUeF6nZmm7pA= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= @@ -342,202 +385,169 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0u go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= go.opentelemetry.io/otel/exporters/prometheus v0.56.0 h1:GnCIi0QyG0yy2MrJLzVrIM7laaJstj//flf1zEJCG+E= go.opentelemetry.io/otel/exporters/prometheus v0.56.0/go.mod h1:JQcVZtbIIPM+7SWBB+T6FK+xunlyidwLp++fN0sUaOk= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= -go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= -go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= -go.temporal.io/api v1.52.0 h1:Tn69z2nhQeXtofa1/j/MbwPHnFRM9+13xqYmFl/KFjM= -go.temporal.io/api v1.52.0/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= -go.temporal.io/sdk v1.35.0 h1:lRNAQ5As9rLgYa7HBvnmKyzxLcdElTuoFJ0FXM/AsLQ= -go.temporal.io/sdk v1.35.0/go.mod h1:1q5MuLc2MEJ4lneZTHJzpVebW2oZnyxoIOWX3oFVebw= -go.temporal.io/server v1.28.1 h1:koDHINsed1onr/TpLfYWINbTBmFQLRUfU5LtPlxjvLQ= -go.temporal.io/server v1.28.1/go.mod h1:QcXPBkDo/WOwq3NPVrT4KsYczsgxvW0bKg489qPn0QU= -go.temporal.io/version v0.3.0 h1:dMrei9l9NyHt8nG6EB8vAwDLLTwx2SvRyucCSumAiig= -go.temporal.io/version v0.3.0/go.mod h1:UA9S8/1LaKYae6TyD9NaPMJTZb911JcbqghI2CBSP78= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= +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.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.temporal.io/api v1.62.8 h1:g8RAZmdebYODoNa2GLA4M4TsXNe1096WV3n26C4+fdw= +go.temporal.io/api v1.62.8/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= +go.temporal.io/sdk v1.41.1 h1:yOpvsHyDD1lNuwlGBv/SUodCPhjv9nDeC9lLHW/fJUA= +go.temporal.io/sdk v1.41.1/go.mod h1:/InXQT5guZ6AizYzpmzr5avQ/GMgq1ZObcKlKE2AhTc= +go.temporal.io/server v1.31.1 h1:3rxA0Ls21hLOseKsyzm7e+IrVLuaDsuri0DGX+chIqU= +go.temporal.io/server v1.31.1/go.mod h1:kOOpZs6WMcLuVmlu0uH+dVD2Ul1d4hGjmGpHjxfz2EI= 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/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= -go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= -go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= -go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU= -go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= 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/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= -go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= -go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= -go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +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.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= -golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= 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.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= -golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 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.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 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.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +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= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/api v0.224.0 h1:Ir4UPtDsNiwIOHdExr3fAj4xZ42QjK7uQte3lORLJwU= -google.golang.org/api v0.224.0/go.mod h1:3V39my2xAGkodXy0vEqcEtkqgw2GtrFL5WuBZlCTCOQ= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= -google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= +google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/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-20180628173108-788fd7840127/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/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/validator.v2 v2.0.1 h1:xF0KWyGWXm/LM2G1TrEjqOu4pa6coO9AlWSf3msVfDY= gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPuG8= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= istio.io/api v1.24.1 h1:jF1I+ABGVS7ImVKzaAeiXHkFEbfXN2IEKDGJTw5UX0w= istio.io/api v1.24.1/go.mod h1:MQnRok7RZ20/PE56v0LxmoWH0xVxnCQPNuf9O7PAN1I= istio.io/client-go v1.24.0 h1:30Qmx12lJCB5xeJuyodPSWh848b2PvgCubdPTazG1eU= istio.io/client-go v1.24.0/go.mod h1:sCDBDJWQGJQz/1t3CHwUTDE5V7Nk6pFFkqBwhIg+LrI= -k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= -k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= -k8s.io/apiextensions-apiserver v0.33.3 h1:qmOcAHN6DjfD0v9kxL5udB27SRP6SG/MTopmge3MwEs= -k8s.io/apiextensions-apiserver v0.33.3/go.mod h1:oROuctgo27mUsyp9+Obahos6CWcMISSAPzQ77CAQGz8= -k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA= -k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA= -k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= -k8s.io/component-base v0.33.3 h1:mlAuyJqyPlKZM7FyaoM/LcunZaaY353RXiOd2+B5tGA= -k8s.io/component-base v0.33.3/go.mod h1:ktBVsBzkI3imDuxYXmVxZ2zxJnYTZ4HAsVj9iF09qp4= +k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= +k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= +k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= +k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= +k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= +k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= +k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= +k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= +k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= +modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY= +modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= sigs.k8s.io/cli-utils v0.35.0 h1:dfSJaF1W0frW74PtjwiyoB4cwdRygbHnC7qe7HF0g/Y= sigs.k8s.io/cli-utils v0.35.0/go.mod h1:ITitykCJxP1vaj1Cew/FZEaVJ2YsTN9Q71m02jebkoE= -sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= -sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= +sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= +sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= sigs.k8s.io/e2e-framework v0.5.0 h1:YLhk8R7EHuTFQAe6Fxy5eBzn5Vb+yamR5u8MH1Rq3cE= sigs.k8s.io/e2e-framework v0.5.0/go.mod h1:jJSH8u2RNmruekUZgHAtmRjb5Wj67GErli9UjLSY7Zc= sigs.k8s.io/gateway-api v1.1.0 h1:DsLDXCi6jR+Xz8/xd0Z1PYl2Pn0TyaFMOPPZIj4inDM= sigs.k8s.io/gateway-api v1.1.0/go.mod h1:ZH4lHrL2sDi0FHZ9jjneb8kKnGzFWyrTya35sWUTrRs= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= -sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= -sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ= -sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/resource/admintools/deployment_builder.go b/internal/resource/admintools/deployment_builder.go index 7bec4487..8a1c537b 100644 --- a/internal/resource/admintools/deployment_builder.go +++ b/internal/resource/admintools/deployment_builder.go @@ -128,7 +128,7 @@ func (b *DeploymentBuilder) Update(object client.Object) error { } deployment.Spec.Template = corev1.PodTemplateSpec{ - ObjectMeta: meta.BuildPodObjectMeta(b.instance, "admintools", b.configHash), + ObjectMeta: meta.BuildPodObjectMeta(b.instance, "admintools", b.configHash, deployment.Spec.Template.ObjectMeta), Spec: corev1.PodSpec{ ImagePullSecrets: b.instance.Spec.ImagePullSecrets, Containers: []corev1.Container{ diff --git a/internal/resource/base/deployment_builder.go b/internal/resource/base/deployment_builder.go index 03a7bafe..cb5ba27a 100644 --- a/internal/resource/base/deployment_builder.go +++ b/internal/resource/base/deployment_builder.go @@ -29,6 +29,7 @@ import ( "github.com/alexandrevilain/temporal-operator/internal/resource/persistence" "github.com/alexandrevilain/temporal-operator/internal/resource/prometheus" "github.com/alexandrevilain/temporal-operator/pkg/kubernetes" + "github.com/alexandrevilain/temporal-operator/pkg/version" "go.temporal.io/server/common/primitives" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -119,6 +120,24 @@ func (b *DeploymentBuilder) Update(object client.Object) error { }, } + // Temporal Server >= 1.30 removed dockerize/auto-setup from the server image. + // The service to start is now selected through the TEMPORAL_SERVICES env var + // (the --service flag) and the config file is located through + // TEMPORAL_SERVER_CONFIG_FILE_PATH. The legacy SERVICES env var is kept for + // backward compatibility and for the metrics "type" tag. + if b.instance.Spec.Version.GreaterOrEqual(version.V1_30_0) { + envVars = append(envVars, + corev1.EnvVar{ + Name: "TEMPORAL_SERVICES", + Value: b.serviceName, + }, + corev1.EnvVar{ + Name: "TEMPORAL_SERVER_CONFIG_FILE_PATH", + Value: meta.ConfigFilePath, + }, + ) + } + datastores := b.instance.Spec.Persistence.GetDatastores() envVars = append(envVars, persistence.GetDatastoresEnvironmentVariables(datastores)...) @@ -126,8 +145,8 @@ func (b *DeploymentBuilder) Update(object client.Object) error { volumeMounts := []corev1.VolumeMount{ { Name: "config", - MountPath: "/etc/temporal/config/config_template.yaml", - SubPath: "config_template.yaml", + MountPath: meta.ConfigFilePath, + SubPath: meta.ConfigFileName, }, } @@ -341,7 +360,7 @@ func (b *DeploymentBuilder) Update(object client.Object) error { } deployment.Spec.Template = corev1.PodTemplateSpec{ - ObjectMeta: meta.BuildPodObjectMeta(b.instance, b.serviceName, b.configHash), + ObjectMeta: meta.BuildPodObjectMeta(b.instance, b.serviceName, b.configHash, deployment.Spec.Template.ObjectMeta), Spec: corev1.PodSpec{ ServiceAccountName: b.instance.ChildResourceName(b.serviceName), DeprecatedServiceAccount: b.instance.ChildResourceName(b.serviceName), diff --git a/internal/resource/config/configmap_builder.go b/internal/resource/config/configmap_builder.go index c2d383fe..89a10d85 100644 --- a/internal/resource/config/configmap_builder.go +++ b/internal/resource/config/configmap_builder.go @@ -49,6 +49,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) +// bindAllAddresses is the address services bind on when they should accept +// connections on every interface in the pod. +const bindAllAddresses = "0.0.0.0" + var _ resource.Builder = (*ConfigmapBuilder)(nil) type ConfigmapBuilder struct { @@ -63,6 +67,31 @@ func NewConfigmapBuilder(instance *v1beta1.TemporalCluster, scheme *runtime.Sche } } +// envPlaceholder returns a config-template placeholder resolving the given +// environment variable at server startup. +// +// Temporal Server >= 1.30 removed dockerize and renders config templates with an +// embedded sprig engine executed with nil template data (see +// go.temporal.io/server/common/config loader). The dockerize-style +// "{{ .Env.NAME }}" syntax therefore no longer resolves and must be replaced by +// the sprig "{{ env \"NAME\" }}" function. +func (b *ConfigmapBuilder) envPlaceholder(name string) string { + if b.instance.Spec.Version.GreaterOrEqual(version.V1_30_0) { + return fmt.Sprintf(`{{ env "%s" }}`, name) + } + return fmt.Sprintf("{{ .Env.%s }}", name) +} + +// broadcastAddressPlaceholder returns the membership broadcast address template, +// defaulting to 0.0.0.0 when POD_IP is unset, using the templating syntax +// supported by the target Temporal Server version. +func (b *ConfigmapBuilder) broadcastAddressPlaceholder() string { + if b.instance.Spec.Version.GreaterOrEqual(version.V1_30_0) { + return `{{ default "0.0.0.0" (env "POD_IP") }}` + } + return `{{ default .Env.POD_IP "0.0.0.0" }}` +} + func (b *ConfigmapBuilder) Build() client.Object { return &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -86,17 +115,21 @@ func (b *ConfigmapBuilder) buildDatastoreConfig(store *v1beta1.DatastoreSpec) (* v1beta1.MySQLDatastore, v1beta1.MySQL8Datastore: cfg.SQL = persistence.NewSQLConfigFromDatastoreSpec(store) - cfg.SQL.Password = fmt.Sprintf("{{ .Env.%s }}", store.GetPasswordEnvVarName()) + // When the datastore resolves its password through an external command + // (Temporal >= 1.31), the static password must be left empty. + if store.SQL.PasswordCommand == nil { + cfg.SQL.Password = b.envPlaceholder(store.GetPasswordEnvVarName()) + } case v1beta1.CassandraDatastore: cfg.Cassandra = persistence.NewCassandraConfigFromDatastoreSpec(store) - cfg.Cassandra.Password = fmt.Sprintf("{{ .Env.%s }}", store.GetPasswordEnvVarName()) + cfg.Cassandra.Password = b.envPlaceholder(store.GetPasswordEnvVarName()) case v1beta1.ElasticsearchDatastore: esCfg, err := persistence.NewElasticsearchConfigFromDatastoreSpec(store) if err != nil { return nil, fmt.Errorf("can't get elasticsearch config: %w", err) } cfg.Elasticsearch = esCfg - cfg.Elasticsearch.Password = fmt.Sprintf("{{ .Env.%s }}", store.GetPasswordEnvVarName()) + cfg.Elasticsearch.Password = b.envPlaceholder(store.GetPasswordEnvVarName()) case v1beta1.UnknownDatastore: return nil, errors.New("unknown datastore") } @@ -215,7 +248,7 @@ func (b *ConfigmapBuilder) Update(object client.Object) error { temporalCfg.Global = temporalconfig.Global{ Membership: temporalconfig.Membership{ MaxJoinDuration: 30 * time.Second, - BroadcastAddress: "{{ default .Env.POD_IP \"0.0.0.0\" }}", + BroadcastAddress: b.broadcastAddressPlaceholder(), }, Authorization: authorization.ToTemporalAuthorization(b.instance.Spec.Authorization), } @@ -245,7 +278,7 @@ func (b *ConfigmapBuilder) Update(object client.Object) error { GRPCPort: int(*b.instance.Spec.Services.Frontend.Port), MembershipPort: int(*b.instance.Spec.Services.Frontend.MembershipPort), BindOnLocalHost: false, - BindOnIP: "0.0.0.0", + BindOnIP: bindAllAddresses, }, }, string(primitives.HistoryService): { @@ -253,7 +286,7 @@ func (b *ConfigmapBuilder) Update(object client.Object) error { GRPCPort: int(*b.instance.Spec.Services.History.Port), MembershipPort: int(*b.instance.Spec.Services.History.MembershipPort), BindOnLocalHost: false, - BindOnIP: "0.0.0.0", + BindOnIP: bindAllAddresses, }, }, string(primitives.MatchingService): { @@ -261,7 +294,7 @@ func (b *ConfigmapBuilder) Update(object client.Object) error { GRPCPort: int(*b.instance.Spec.Services.Matching.Port), MembershipPort: int(*b.instance.Spec.Services.Matching.MembershipPort), BindOnLocalHost: false, - BindOnIP: "0.0.0.0", + BindOnIP: bindAllAddresses, }, }, string(primitives.WorkerService): { @@ -269,7 +302,7 @@ func (b *ConfigmapBuilder) Update(object client.Object) error { GRPCPort: int(*b.instance.Spec.Services.Worker.Port), MembershipPort: int(*b.instance.Spec.Services.Worker.MembershipPort), BindOnLocalHost: false, - BindOnIP: "0.0.0.0", + BindOnIP: bindAllAddresses, }, }, } @@ -282,7 +315,7 @@ func (b *ConfigmapBuilder) Update(object client.Object) error { MembershipPort: int(*b.instance.Spec.Services.InternalFrontend.MembershipPort), HTTPPort: int(*b.instance.Spec.Services.InternalFrontend.HTTPPort), BindOnLocalHost: false, - BindOnIP: "0.0.0.0", + BindOnIP: bindAllAddresses, }, } } @@ -314,7 +347,7 @@ func (b *ConfigmapBuilder) Update(object client.Object) error { if b.instance.Spec.Metrics.IsEnabled() { temporalCfg.Global.Metrics = &metrics.Config{ ClientConfig: metrics.ClientConfig{ - Tags: map[string]string{"type": "{{ .Env.SERVICES }}"}, + Tags: map[string]string{"type": b.envPlaceholder("SERVICES")}, }, } @@ -441,8 +474,17 @@ func (b *ConfigmapBuilder) Update(object client.Object) error { return fmt.Errorf("failed marshaling temporal config: %w", err) } + renderedConfig := string(result) + + // Temporal Server >= 1.30 renders config templates with an embedded sprig + // engine (dockerize was removed). Templating is only activated when the file + // starts with an "enable-template" comment within its first 1KB. + if b.instance.Spec.Version.GreaterOrEqual(version.V1_30_0) { + renderedConfig = "# enable-template\n" + renderedConfig + } + configMap.Data = map[string]string{ - "config_template.yaml": string(result), + meta.ConfigFileName: renderedConfig, } if err := controllerutil.SetControllerReference(b.instance, configMap, b.scheme); err != nil { diff --git a/internal/resource/config/configmap_builder_test.go b/internal/resource/config/configmap_builder_test.go new file mode 100644 index 00000000..9efdb4a8 --- /dev/null +++ b/internal/resource/config/configmap_builder_test.go @@ -0,0 +1,152 @@ +// Licensed to Alexandre VILAIN under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Alexandre VILAIN licenses this file to you 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 config_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/alexandrevilain/temporal-operator/api/v1beta1" + "github.com/alexandrevilain/temporal-operator/internal/resource/config" + "github.com/alexandrevilain/temporal-operator/pkg/version" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + temporalconfig "go.temporal.io/server/common/config" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func newSQLCluster(t *testing.T, v string) *v1beta1.TemporalCluster { + t.Helper() + + store := func() *v1beta1.DatastoreSpec { + return &v1beta1.DatastoreSpec{ + SQL: &v1beta1.SQLSpec{ + User: "temporal", + PluginName: "postgres12", + DatabaseName: "temporal", + ConnectAddr: "postgres:5432", + ConnectProtocol: "tcp", + }, + } + } + + cluster := &v1beta1.TemporalCluster{ + TypeMeta: v1beta1.TemporalClusterTypeMeta, + ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, + Spec: v1beta1.TemporalClusterSpec{ + Version: version.MustNewVersionFromString(v), + NumHistoryShards: 1, + Persistence: v1beta1.TemporalPersistenceSpec{ + DefaultStore: store(), + VisibilityStore: store(), + }, + }, + } + cluster.Spec.Persistence.DefaultStore.Name = "default" + cluster.Spec.Persistence.VisibilityStore.Name = "visibility" + cluster.Default() + + return cluster +} + +func buildConfigTemplate(t *testing.T, cluster *v1beta1.TemporalCluster) string { + t.Helper() + + scheme := runtime.NewScheme() + require.NoError(t, corev1.AddToScheme(scheme)) + require.NoError(t, v1beta1.AddToScheme(scheme)) + + builder := config.NewConfigmapBuilder(cluster, scheme) + obj := builder.Build() + require.NoError(t, builder.Update(obj)) + + cm, ok := obj.(*corev1.ConfigMap) + require.True(t, ok, "expected a ConfigMap") + + tmpl, ok := cm.Data["config_template.yaml"] + require.True(t, ok, "config_template.yaml key must be present") + + return tmpl +} + +// TestConfigTemplating_Pre130 asserts that clusters older than 1.30 keep using +// the dockerize "{{ .Env.X }}" placeholders and do not carry the sprig +// enable-template header. +func TestConfigTemplating_Pre130(t *testing.T) { + tmpl := buildConfigTemplate(t, newSQLCluster(t, "1.29.7")) + + assert.False(t, strings.HasPrefix(tmpl, "# enable-template"), + "pre-1.30 config must not enable server-side templating") + assert.Contains(t, tmpl, "{{ .Env.", "pre-1.30 config must use dockerize placeholders") + assert.NotContains(t, tmpl, `{{ env "`, "pre-1.30 config must not use sprig env placeholders") +} + +// TestConfigTemplating_Post130 asserts that clusters >= 1.30 emit the sprig +// enable-template header and env placeholders, and that the rendered template +// is accepted by the real Temporal Server config loader (which embeds sprig and +// removed dockerize in 1.30). +func TestConfigTemplating_Post130(t *testing.T) { + tmpl := buildConfigTemplate(t, newSQLCluster(t, "1.30.5")) + + assert.True(t, strings.HasPrefix(tmpl, "# enable-template"), + "1.30+ config must start with the enable-template header") + assert.Contains(t, tmpl, `{{ env "`, "1.30+ config must use sprig env placeholders") + assert.NotContains(t, tmpl, "{{ .Env.", "1.30+ config must not use dockerize placeholders") + + // Feed the generated template to the actual server config loader to prove it + // parses, renders (sprig) and unmarshals under Temporal Server >= 1.30. + dir := t.TempDir() + path := filepath.Join(dir, "config.yaml") + require.NoError(t, os.WriteFile(path, []byte(tmpl), 0o600)) + + cfg, err := temporalconfig.Load(temporalconfig.WithConfigFile(path)) + require.NoError(t, err, "generated 1.30 config must load via the server config loader") + assert.NotNil(t, cfg) +} + +// TestConfigPasswordCommand asserts that a 1.31 cluster whose datastore uses an +// external passwordCommand renders it into the server config (and omits the +// static password placeholder), and that the result loads via the real server +// config loader. +func TestConfigPasswordCommand(t *testing.T) { + cluster := newSQLCluster(t, "1.31.1") + cluster.Spec.Persistence.DefaultStore.SQL.PasswordCommand = &v1beta1.SQLPasswordCommandSpec{ + Command: "/bin/echo", + Args: []string{"token"}, + } + + tmpl := buildConfigTemplate(t, cluster) + + assert.Contains(t, tmpl, "passwordCommand:", "passwordCommand must be rendered") + assert.Contains(t, tmpl, "/bin/echo", "passwordCommand command must be rendered") + + dir := t.TempDir() + path := filepath.Join(dir, "config.yaml") + require.NoError(t, os.WriteFile(path, []byte(tmpl), 0o600)) + + cfg, err := temporalconfig.Load(temporalconfig.WithConfigFile(path)) + require.NoError(t, err, "generated 1.31 passwordCommand config must load") + require.NotNil(t, cfg) + require.NotNil(t, cfg.Persistence.DataStores["default"].SQL) + assert.NotNil(t, cfg.Persistence.DataStores["default"].SQL.PasswordCommand, + "loaded config must carry the passwordCommand") +} diff --git a/internal/resource/meta/names.go b/internal/resource/meta/names.go index fb30ffb5..0f4cf9d0 100644 --- a/internal/resource/meta/names.go +++ b/internal/resource/meta/names.go @@ -29,3 +29,18 @@ const ( ServiceUIName = "ui" ServiceAdminTools = "admintools" ) + +// Server config file location. +// +// These three values have to agree: the rendered config is stored in the config +// ConfigMap under ConfigFileName, mounted into the server container at +// ConfigFilePath, and — for Temporal >= 1.30, which no longer has a fixed +// built-in location — found by the server through the +// TEMPORAL_SERVER_CONFIG_FILE_PATH environment variable. If they drift apart the +// server exits at startup with "could not read config file", so they are +// defined once here rather than repeated at each use. +const ( + ConfigFileName = "config_template.yaml" + ConfigMountDir = "/etc/temporal/config" + ConfigFilePath = ConfigMountDir + "/" + ConfigFileName +) diff --git a/internal/resource/meta/pod.go b/internal/resource/meta/pod.go index ab996e7d..791ab985 100644 --- a/internal/resource/meta/pod.go +++ b/internal/resource/meta/pod.go @@ -18,6 +18,8 @@ package meta import ( + "strings" + "github.com/alexandrevilain/temporal-operator/api/v1beta1" "github.com/alexandrevilain/temporal-operator/internal/metadata" "github.com/alexandrevilain/temporal-operator/internal/resource/mtls/istio" @@ -30,18 +32,71 @@ const ( configHashKey = "operator.temporal.io/config" ) +// managedLabelPrefixes and managedAnnotationPrefixes list the pod-template +// metadata namespaces this operator computes from the cluster spec. +// +// Keys under these prefixes are dropped from the existing pod template before +// the freshly computed metadata is overlaid. This is what makes removal work: +// the feature helpers (istio, linkerd, prometheus) return an *empty* map when +// their feature is disabled rather than a removal signal, so a plain merge over +// the existing metadata would make every operator-managed key permanent. For +// example, clearing spec.mTLS after using the istio provider would leave +// `sidecar.istio.io/inject: "true"` behind and istio would keep injecting +// sidecars forever. +// +// Anything outside these namespaces is preserved untouched, which is the point +// of merging with the existing metadata at all: annotations written by other +// actors (notably `kubectl.kubernetes.io/restartedAt` from `kubectl rollout +// restart`) must survive reconciliation. +var ( + managedLabelPrefixes = []string{ + "app.kubernetes.io/", // metadata.GetLabels + "sidecar.istio.io/", // istio.GetLabels + } + + managedAnnotationPrefixes = []string{ + "linkerd.io/", // linkerd.GetAnnotations + "proxy.istio.io/", // istio.GetAnnotations + "prometheus.io/", // prometheus.GetAnnotations + "operator.temporal.io/", // configHashKey + } +) + +// dropManaged returns a copy of m without the keys this operator computes, so +// that stale ones do not survive into the merged result. +func dropManaged(m map[string]string, prefixes []string) map[string]string { + return metadata.FilterAnnotations(m, func(k, _ string) bool { + for _, prefix := range prefixes { + if strings.HasPrefix(k, prefix) { + return false + } + } + return true + }) +} + // BuildPodObjectMeta return ObjectMeta for the service (frontend, ui, admintools) of the provided Cluster. -func BuildPodObjectMeta(instance *v1beta1.TemporalCluster, service, configHash string) metav1.ObjectMeta { +// It merges existing pod template labels and annotations with the operator-managed ones, +// so that externally-added annotations (e.g. from kubectl rollout restart) are preserved. +// +// Note: labels and annotations copied from the TemporalCluster's own metadata +// are re-derived from the spec on every call, so they track additions and edits +// there. Removing one from the TemporalCluster does not remove it from existing +// pod templates unless it falls under a managed prefix, since nothing records +// which keys a previous reconcile propagated. +func BuildPodObjectMeta(instance *v1beta1.TemporalCluster, service, configHash string, existing metav1.ObjectMeta) metav1.ObjectMeta { instanceAnnotations := metadata.FilterAnnotations(instance.Annotations, func(k, _ string) bool { return k != "kubectl.kubernetes.io/last-applied-configuration" }) return metav1.ObjectMeta{ Labels: metadata.Merge( + dropManaged(existing.Labels, managedLabelPrefixes), istio.GetLabels(instance), metadata.GetLabels(instance, service, instance.Spec.Version, instance.Labels), ), Annotations: metadata.Merge( + dropManaged(existing.Annotations, managedAnnotationPrefixes), linkerd.GetAnnotations(instance), istio.GetAnnotations(instance), prometheus.GetAnnotations(instance), diff --git a/internal/resource/meta/pod_test.go b/internal/resource/meta/pod_test.go new file mode 100644 index 00000000..40bfa3fd --- /dev/null +++ b/internal/resource/meta/pod_test.go @@ -0,0 +1,181 @@ +// Licensed to Alexandre VILAIN under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Alexandre VILAIN licenses this file to you 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 meta_test + +import ( + "testing" + + "github.com/alexandrevilain/temporal-operator/api/v1beta1" + "github.com/alexandrevilain/temporal-operator/internal/resource/meta" + "github.com/alexandrevilain/temporal-operator/pkg/version" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func newTestCluster() *v1beta1.TemporalCluster { + return &v1beta1.TemporalCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + Namespace: "default", + }, + Spec: v1beta1.TemporalClusterSpec{ + Version: version.MustNewVersionFromString("1.24.1"), + }, + } +} + +func TestBuildPodObjectMeta_PreservesExistingAnnotations(t *testing.T) { + instance := newTestCluster() + existing := metav1.ObjectMeta{ + Annotations: map[string]string{ + "kubectl.kubernetes.io/restartedAt": "2024-01-01T00:00:00Z", + "custom-annotation": "custom-value", + }, + } + + result := meta.BuildPodObjectMeta(instance, "frontend", "abc123", existing) + + assert.Equal(t, "2024-01-01T00:00:00Z", result.Annotations["kubectl.kubernetes.io/restartedAt"]) + assert.Equal(t, "custom-value", result.Annotations["custom-annotation"]) + assert.Equal(t, "abc123", result.Annotations["operator.temporal.io/config"]) +} + +func TestBuildPodObjectMeta_PreservesExistingLabels(t *testing.T) { + instance := newTestCluster() + existing := metav1.ObjectMeta{ + Labels: map[string]string{ + "custom-label": "custom-value", + }, + } + + result := meta.BuildPodObjectMeta(instance, "frontend", "abc123", existing) + + assert.Equal(t, "custom-value", result.Labels["custom-label"]) + assert.Equal(t, "test-cluster", result.Labels["app.kubernetes.io/name"]) + assert.Equal(t, "frontend", result.Labels["app.kubernetes.io/component"]) +} + +func TestBuildPodObjectMeta_OperatorAnnotationsOverrideExisting(t *testing.T) { + instance := newTestCluster() + existing := metav1.ObjectMeta{ + Annotations: map[string]string{ + "operator.temporal.io/config": "old-hash", + }, + } + + result := meta.BuildPodObjectMeta(instance, "frontend", "new-hash", existing) + + assert.Equal(t, "new-hash", result.Annotations["operator.temporal.io/config"]) +} + +func TestBuildPodObjectMeta_EmptyExistingObjectMeta(t *testing.T) { + instance := newTestCluster() + existing := metav1.ObjectMeta{} + + result := meta.BuildPodObjectMeta(instance, "frontend", "abc123", existing) + + assert.Equal(t, "abc123", result.Annotations["operator.temporal.io/config"]) + assert.Equal(t, "test-cluster", result.Labels["app.kubernetes.io/name"]) + assert.Equal(t, "frontend", result.Labels["app.kubernetes.io/component"]) +} + +// The tests above cover the additive direction. The ones below cover removal: +// the mTLS/metrics helpers return an empty map when their feature is disabled +// rather than a removal signal, so merging over the existing pod template must +// not make operator-managed keys permanent. + +func TestBuildPodObjectMeta_RemovesIstioMetadataWhenMTLSCleared(t *testing.T) { + instance := newTestCluster() + // mTLS is not set on the instance, but the live pod template still carries + // the istio metadata from when it was. + existing := metav1.ObjectMeta{ + Labels: map[string]string{ + "sidecar.istio.io/inject": "true", + "custom-label": "custom-value", + }, + Annotations: map[string]string{ + "proxy.istio.io/config": `{ "holdApplicationUntilProxyStarts": true }`, + "custom-annotation": "custom-value", + }, + } + + result := meta.BuildPodObjectMeta(instance, "frontend", "abc123", existing) + + assert.NotContains(t, result.Labels, "sidecar.istio.io/inject") + assert.NotContains(t, result.Annotations, "proxy.istio.io/config") + // Metadata the operator does not manage is still preserved. + assert.Equal(t, "custom-value", result.Labels["custom-label"]) + assert.Equal(t, "custom-value", result.Annotations["custom-annotation"]) +} + +func TestBuildPodObjectMeta_RemovesLinkerdAnnotationWhenMTLSCleared(t *testing.T) { + instance := newTestCluster() + existing := metav1.ObjectMeta{ + Annotations: map[string]string{ + "linkerd.io/inject": "enabled", + }, + } + + result := meta.BuildPodObjectMeta(instance, "frontend", "abc123", existing) + + assert.NotContains(t, result.Annotations, "linkerd.io/inject") +} + +func TestBuildPodObjectMeta_RemovesPrometheusAnnotationsWhenMetricsDisabled(t *testing.T) { + instance := newTestCluster() + existing := metav1.ObjectMeta{ + Annotations: map[string]string{ + "prometheus.io/scrape": "true", + "prometheus.io/scheme": "http", + "prometheus.io/path": "/metrics", + "prometheus.io/port": "9090", + }, + } + + result := meta.BuildPodObjectMeta(instance, "frontend", "abc123", existing) + + for _, key := range []string{"prometheus.io/scrape", "prometheus.io/scheme", "prometheus.io/path", "prometheus.io/port"} { + assert.NotContains(t, result.Annotations, key) + } +} + +func TestBuildPodObjectMeta_KeepsIstioMetadataWhileMTLSEnabled(t *testing.T) { + instance := newTestCluster() + instance.Spec.MTLS = &v1beta1.MTLSSpec{Provider: v1beta1.IstioMTLSProvider} + existing := metav1.ObjectMeta{} + + result := meta.BuildPodObjectMeta(instance, "frontend", "abc123", existing) + + // Stripping managed keys must not defeat the feature while it is enabled: + // the value is recomputed from the spec on every call. + assert.Equal(t, "true", result.Labels["sidecar.istio.io/inject"]) + assert.Contains(t, result.Annotations, "proxy.istio.io/config") +} + +func TestBuildPodObjectMeta_RemovesStaleVersionLabel(t *testing.T) { + instance := newTestCluster() + existing := metav1.ObjectMeta{ + Labels: map[string]string{ + "app.kubernetes.io/version": "1.20.0", + }, + } + + result := meta.BuildPodObjectMeta(instance, "frontend", "abc123", existing) + + assert.Equal(t, "1.24.1", result.Labels["app.kubernetes.io/version"]) +} diff --git a/internal/resource/persistence/schema_scripts_configmap_builder.go b/internal/resource/persistence/schema_scripts_configmap_builder.go index 94e9aaa7..01f69ad3 100644 --- a/internal/resource/persistence/schema_scripts_configmap_builder.go +++ b/internal/resource/persistence/schema_scripts_configmap_builder.go @@ -109,6 +109,10 @@ func (b *SchemaScriptsConfigmapBuilder) baseData() baseData { baseData.MTLSProvider = string(b.instance.Spec.MTLS.Provider) } + // Temporal >= 1.30 dropped curl from the admin-tools image; the MTLS + // sidecar-shutdown footer must use busybox wget instead. + baseData.UseWget = b.instance.Spec.Version.GreaterOrEqual(version.V1_30_0) + return baseData } @@ -159,6 +163,23 @@ func (b *SchemaScriptsConfigmapBuilder) argsMapToString(m *orderedmap.OrderedMap return strings.Join(cmd, " ") } +// shellCommand renders an external password command and its arguments as a +// single POSIX-shell string suitable for command substitution "$( ... )". +func shellCommand(pc *v1beta1.SQLPasswordCommandSpec) string { + parts := make([]string, 0, len(pc.Args)+1) + parts = append(parts, shellQuote(pc.Command)) + for _, a := range pc.Args { + parts = append(parts, shellQuote(a)) + } + return strings.Join(parts, " ") +} + +// shellQuote wraps s in single quotes, escaping any embedded single quotes, so +// it is safe to embed in a shell command line. +func shellQuote(s string) string { + return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" +} + func (b *SchemaScriptsConfigmapBuilder) getSQLArgs(spec *v1beta1.DatastoreSpec) (*orderedmap.OrderedMap[string, string], error) { host, port, err := net.SplitHostPort(spec.SQL.ConnectAddr) if err != nil { @@ -166,11 +187,21 @@ func (b *SchemaScriptsConfigmapBuilder) getSQLArgs(spec *v1beta1.DatastoreSpec) } args := orderedmap.NewOrderedMap[string, string]() - args.Set(schema.CLIOptEndpoint, host) // --endpoint - args.Set(schema.CLIOptPort, port) // --port - args.Set(schema.CLIOptUser, spec.SQL.User) // --user - if spec.PasswordSecretRef != nil { + args.Set(schema.CLIOptEndpoint, host) // --endpoint + args.Set(schema.CLIOptPort, port) // --port + // Omit --user when empty: a bare flag would swallow the next token as its + // value. See getElasticsearchArgs for the full explanation. + if spec.SQL.User != "" { + args.Set(schema.CLIOptUser, spec.SQL.User) // --user + } + switch { + case spec.PasswordSecretRef != nil: args.Set(schema.CLIOptPassword, fmt.Sprintf("$%s", spec.GetPasswordEnvVarName())) // --password + case spec.SQL.PasswordCommand != nil: + // The datastore resolves its password by running an external command + // (Temporal >= 1.31). The schema tool has no equivalent flag, so the + // generated shell script resolves it via command substitution at runtime. + args.Set(schema.CLIOptPassword, fmt.Sprintf("$(%s)", shellCommand(spec.SQL.PasswordCommand))) // --password } args.Set(schema.CLIOptDatabase, spec.SQL.DatabaseName) // --database args.Set(schema.CLIOptPluginName, spec.SQL.PluginName) // --plugin @@ -193,7 +224,11 @@ func (b *SchemaScriptsConfigmapBuilder) getCassandraArgs(spec *v1beta1.Datastore args := orderedmap.NewOrderedMap[string, string]() args.Set(schema.CLIOptEndpoint, strings.Join(spec.Cassandra.Hosts, ",")) args.Set(schema.CLIOptPort, strconv.Itoa(spec.Cassandra.Port)) - args.Set(schema.CLIOptUser, spec.Cassandra.User) + // Omit --user when empty: a bare flag would swallow the next token as its + // value. See getElasticsearchArgs for the full explanation. + if spec.Cassandra.User != "" { + args.Set(schema.CLIOptUser, spec.Cassandra.User) + } args.Set(schema.CLIOptPassword, fmt.Sprintf("$%s", spec.GetPasswordEnvVarName())) args.Set(schema.CLIOptKeyspace, spec.Cassandra.Keyspace) if spec.Cassandra.Datacenter != "" { @@ -213,6 +248,28 @@ func (b *SchemaScriptsConfigmapBuilder) getCassandraArgs(spec *v1beta1.Datastore return args } +// getElasticsearchArgs builds the connection flags for temporal-elasticsearch-tool +// (Temporal >= 1.30). TLS flags are appended by the shared block in getStoreArgs. +func (b *SchemaScriptsConfigmapBuilder) getElasticsearchArgs(spec *v1beta1.DatastoreSpec) *orderedmap.OrderedMap[string, string] { + args := orderedmap.NewOrderedMap[string, string]() + args.Set(schema.CLIOptEndpoint, spec.Elasticsearch.URL) // --endpoint + // Only set --user when it has a value. argsMapToString renders an empty + // value as a bare "--user", and the tool's flag parser (urfave/cli v1, on + // top of the stdlib flag package) then takes the *following* token as the + // flag's value. For "setup-schema" that swallows the subcommand itself: + // the tool finds no command, prints its help and exits 0, so the job is + // reported successful while neither the index template nor the index was + // ever created. An empty username is what an auth-less Elasticsearch needs, + // and the CRD permits it, so this is reachable. + if spec.Elasticsearch.Username != "" { + args.Set(schema.CLIOptUser, spec.Elasticsearch.Username) // --user + } + if spec.PasswordSecretRef != nil { + args.Set(schema.CLIOptPassword, fmt.Sprintf("$%s", spec.GetPasswordEnvVarName())) // --password + } + return args +} + func (b *SchemaScriptsConfigmapBuilder) getStoreArgs(spec *v1beta1.DatastoreSpec) (*orderedmap.OrderedMap[string, string], error) { var args *orderedmap.OrderedMap[string, string] var err error @@ -228,7 +285,9 @@ func (b *SchemaScriptsConfigmapBuilder) getStoreArgs(spec *v1beta1.DatastoreSpec if err != nil { return nil, err } - case v1beta1.ElasticsearchDatastore, v1beta1.UnknownDatastore: + case v1beta1.ElasticsearchDatastore: + args = b.getElasticsearchArgs(spec) + case v1beta1.UnknownDatastore: return nil, fmt.Errorf("unsupported datastore: %s", spec.GetType()) } @@ -270,12 +329,27 @@ func (b *SchemaScriptsConfigmapBuilder) getStoreTool(storeType v1beta1.Datastore // Fix for https://github.com/temporalio/temporal/blob/master/tools/cassandra/main.go#L70 // Which requires an env var set. tool = "CASSANDRA_PORT=9042 temporal-cassandra-tool" - case v1beta1.UnknownDatastore, v1beta1.ElasticsearchDatastore: + case v1beta1.ElasticsearchDatastore: + // temporal-elasticsearch-tool only exists in admin-tools >= 1.30. Older + // images drive Elasticsearch through the curl/jq templates, which do not + // reference a tool at all, so keep returning the empty sentinel there + // rather than emitting a binary that is not present in the image. + if b.esToolAvailable() { + tool = "temporal-elasticsearch-tool" + } + case v1beta1.UnknownDatastore: tool = "" } return tool } +// esToolAvailable reports whether the cluster's admin-tools image ships +// temporal-elasticsearch-tool. Temporal >= 1.30 removed curl and jq from that +// image and added the tool as the supported way to manage the visibility index. +func (b *SchemaScriptsConfigmapBuilder) esToolAvailable() bool { + return b.instance.Spec.Version.GreaterOrEqual(version.V1_30_0) +} + func (b *SchemaScriptsConfigmapBuilder) getESVersion(es *v1beta1.ElasticsearchSpec) string { version := es.Version if version == "v8" { @@ -337,6 +411,21 @@ func (b *SchemaScriptsConfigmapBuilder) GetStoreCreateTemplate(spec *v1beta1.Dat func (b *SchemaScriptsConfigmapBuilder) GetStoreSetupTemplate(spec *v1beta1.DatastoreSpec) (string, error) { storeType := spec.GetType() if storeType == v1beta1.ElasticsearchDatastore { + // Temporal >= 1.30 uses temporal-elasticsearch-tool (curl/jq removed from the image). + if b.esToolAvailable() { + // getStoreArgs routes ES to getElasticsearchArgs and appends the shared + // TLS flags, so the tool gets --tls/--tls-*-file when the store uses TLS. + args, err := b.getStoreArgs(spec) + if err != nil { + return "", fmt.Errorf("can't get store args: %w", err) + } + return b.renderTemplate(setupESVisibilityTool, esToolData{ + baseData: b.baseData(), + Tool: b.getStoreTool(storeType), + ConnectionArgs: b.argsMapToString(args), + Indices: spec.Elasticsearch.Indices, + }) + } data := esSchemaData{ baseData: b.baseData(), Version: b.getESVersion(spec.Elasticsearch), @@ -366,6 +455,19 @@ func (b *SchemaScriptsConfigmapBuilder) GetStoreSetupTemplate(spec *v1beta1.Data func (b *SchemaScriptsConfigmapBuilder) GetStoreUpdateTemplate(spec *v1beta1.DatastoreSpec, targetSchema Schema) (string, error) { storeType := spec.GetType() if storeType == v1beta1.ElasticsearchDatastore { + // Temporal >= 1.30 uses temporal-elasticsearch-tool (curl/jq removed from the image). + if b.esToolAvailable() { + args, err := b.getStoreArgs(spec) + if err != nil { + return "", fmt.Errorf("can't get store args: %w", err) + } + return b.renderTemplate(updateESVisibilityTool, esToolData{ + baseData: b.baseData(), + Tool: b.getStoreTool(storeType), + ConnectionArgs: b.argsMapToString(args), + Indices: spec.Elasticsearch.Indices, + }) + } data := esSchemaData{ baseData: b.baseData(), Version: b.getESVersion(spec.Elasticsearch), diff --git a/internal/resource/persistence/schema_scripts_configmap_builder_test.go b/internal/resource/persistence/schema_scripts_configmap_builder_test.go new file mode 100644 index 00000000..c7c43cd8 --- /dev/null +++ b/internal/resource/persistence/schema_scripts_configmap_builder_test.go @@ -0,0 +1,253 @@ +// Licensed to Alexandre VILAIN under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Alexandre VILAIN licenses this file to you 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 persistence + +import ( + "testing" + + "github.com/alexandrevilain/temporal-operator/api/v1beta1" + "github.com/alexandrevilain/temporal-operator/pkg/version" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestShellQuote(t *testing.T) { + assert.Equal(t, `'foo'`, shellQuote("foo")) + assert.Equal(t, `'a b'`, shellQuote("a b")) + // embedded single quote is escaped + assert.Equal(t, `'it'\''s'`, shellQuote("it's")) +} + +// TestGetSQLArgs_PasswordCommand asserts that a datastore using an external +// passwordCommand renders the schema tool --password flag as a shell command +// substitution (resolved at runtime by the generated setup script), instead of +// referencing a password environment variable. +func TestGetSQLArgs_PasswordCommand(t *testing.T) { + b := &SchemaScriptsConfigmapBuilder{} + + spec := &v1beta1.DatastoreSpec{ + Name: "default", + SQL: &v1beta1.SQLSpec{ + User: "temporal", + PluginName: "postgres12", + DatabaseName: "temporal", + ConnectAddr: "postgres:5432", + PasswordCommand: &v1beta1.SQLPasswordCommandSpec{ + Command: "/bin/sh", + Args: []string{"-c", "printf %s test"}, + }, + }, + } + + args, err := b.getSQLArgs(spec) + require.NoError(t, err) + + rendered := b.argsMapToString(args) + assert.Contains(t, rendered, `--password="$('/bin/sh' '-c' 'printf %s test')"`, + "passwordCommand must render as a shell command substitution") + assert.NotContains(t, rendered, "PASSWORD", + "passwordCommand must not reference a password env var") +} + +// TestGetSQLArgs_PasswordSecretRef keeps asserting the classic secret-based path +// still renders a password env var reference. +func TestGetSQLArgs_PasswordSecretRef(t *testing.T) { + b := &SchemaScriptsConfigmapBuilder{} + + spec := &v1beta1.DatastoreSpec{ + Name: "default", + SQL: &v1beta1.SQLSpec{ + User: "temporal", + PluginName: "postgres12", + DatabaseName: "temporal", + ConnectAddr: "postgres:5432", + }, + PasswordSecretRef: &v1beta1.SecretKeyReference{Name: "postgres-password", Key: "PASSWORD"}, + } + + args, err := b.getSQLArgs(spec) + require.NoError(t, err) + + rendered := b.argsMapToString(args) + assert.Contains(t, rendered, "--password=\"$"+spec.GetPasswordEnvVarName()+"\"") +} + +func esVisibilityStore() *v1beta1.DatastoreSpec { + return &v1beta1.DatastoreSpec{ + Name: "visibility", + Elasticsearch: &v1beta1.ElasticsearchSpec{ + URL: "http://elasticsearch:9200", + Username: "elastic", + Indices: v1beta1.ElasticsearchIndices{Visibility: "temporal_visibility_v1_dev"}, + }, + PasswordSecretRef: &v1beta1.SecretKeyReference{Name: "es-password", Key: "PASSWORD"}, + } +} + +func esBuilder(v string) *SchemaScriptsConfigmapBuilder { + return &SchemaScriptsConfigmapBuilder{ + instance: &v1beta1.TemporalCluster{ + Spec: v1beta1.TemporalClusterSpec{ + Version: version.MustNewVersionFromString(v), + }, + }, + } +} + +// TestESVisibility_Tool_Post130 asserts that on Temporal >= 1.30 the ES visibility +// setup/update scripts drive temporal-elasticsearch-tool (curl/jq were removed from +// the admin-tools image) instead of curl. +func TestESVisibility_Tool_Post130(t *testing.T) { + b := esBuilder("1.30.5") + store := esVisibilityStore() + + setup, err := b.GetStoreSetupTemplate(store) + require.NoError(t, err) + assert.Contains(t, setup, "temporal-elasticsearch-tool") + assert.Contains(t, setup, "setup-schema") + assert.Contains(t, setup, `create-index --index "temporal_visibility_v1_dev"`) + assert.Contains(t, setup, `--endpoint="http://elasticsearch:9200"`) + assert.Contains(t, setup, `--user="elastic"`) + assert.Contains(t, setup, "--password=\"$"+store.GetPasswordEnvVarName()+"\"") + assert.NotContains(t, setup, "curl") + + update, err := b.GetStoreUpdateTemplate(store, VisibilitySchema) + require.NoError(t, err) + assert.Contains(t, update, "temporal-elasticsearch-tool") + assert.Contains(t, update, `update-schema --index "temporal_visibility_v1_dev"`) + assert.NotContains(t, update, "curl") +} + +// TestESVisibility_Curl_Pre130 asserts that on Temporal < 1.30 the legacy curl-based +// scripts are still generated (older admin-tools images ship curl and lack the tool). +func TestESVisibility_Curl_Pre130(t *testing.T) { + b := esBuilder("1.29.7") + store := esVisibilityStore() + + setup, err := b.GetStoreSetupTemplate(store) + require.NoError(t, err) + assert.Contains(t, setup, "curl") + assert.Contains(t, setup, "_template") + assert.NotContains(t, setup, "temporal-elasticsearch-tool") +} + +// TestESVisibility_EmptyUsername_NoBareUserFlag guards a silent-failure mode. +// +// argsMapToString renders an empty value as a bare "--user", and the tool's +// flag parser (urfave/cli v1 over the stdlib flag package) then takes the +// following token as that flag's value. For the setup script that token is the +// "setup-schema" subcommand itself, so the tool finds no command at all, prints +// its help and exits 0 — the schema job is recorded as successful while neither +// the index template nor the visibility index was ever created. +// +// An empty username is what an auth-less Elasticsearch needs and the CRD allows +// it (required, but with no minimum length), so this is reachable. +func TestESVisibility_EmptyUsername_NoBareUserFlag(t *testing.T) { + b := esBuilder("1.30.5") + store := esVisibilityStore() + store.Elasticsearch.Username = "" + store.PasswordSecretRef = nil + + setup, err := b.GetStoreSetupTemplate(store) + require.NoError(t, err) + assert.NotRegexp(t, `--user(\s|$)`, setup, "empty username must not render a bare --user flag") + assert.NotRegexp(t, `--password(\s|$)`, setup, "absent password must not render a bare --password flag") + assert.Contains(t, setup, "setup-schema") + + update, err := b.GetStoreUpdateTemplate(store, VisibilitySchema) + require.NoError(t, err) + assert.NotRegexp(t, `--user(\s|$)`, update) +} + +// TestSQLAndCassandraArgs_EmptyUser_NoBareUserFlag covers the same rendering +// hazard on the other two datastore families. +func TestSQLAndCassandraArgs_EmptyUser_NoBareUserFlag(t *testing.T) { + b := &SchemaScriptsConfigmapBuilder{} + + sqlArgs, err := b.getSQLArgs(&v1beta1.DatastoreSpec{ + Name: "default", + SQL: &v1beta1.SQLSpec{ + User: "", + PluginName: "postgres12", + DatabaseName: "temporal", + ConnectAddr: "postgres:5432", + }, + }) + require.NoError(t, err) + assert.NotRegexp(t, `--user(\s|$)`, b.argsMapToString(sqlArgs)) + + cassandraArgs := b.getCassandraArgs(&v1beta1.DatastoreSpec{ + Name: "default", + Cassandra: &v1beta1.CassandraSpec{Hosts: []string{"cassandra"}, Port: 9042, User: "", Keyspace: "temporal"}, + }) + assert.NotRegexp(t, `--user(\s|$)`, b.argsMapToString(cassandraArgs)) +} + +// TestGetStoreTool_ElasticsearchPre130 asserts the empty sentinel is preserved +// below 1.30. temporal-elasticsearch-tool does not exist in those admin-tools +// images, so a caller that reaches getStoreTool without checking the version +// must not be handed a binary name that cannot be executed. +func TestGetStoreTool_ElasticsearchPre130(t *testing.T) { + assert.Empty(t, esBuilder("1.29.7").getStoreTool(v1beta1.ElasticsearchDatastore)) + assert.Equal(t, "temporal-elasticsearch-tool", esBuilder("1.30.5").getStoreTool(v1beta1.ElasticsearchDatastore)) +} + +// TestESVisibility_Tool_ShutdownFooterReachableOnFailure asserts the generated +// script cannot abort before the service-mesh shutdown footer. +// +// With "set -e", a failing temporal-elasticsearch-tool exits the script +// immediately and the footer never posts to the linkerd proxy's /shutdown. The +// sidecar then keeps the Job pod Running indefinitely rather than letting it +// fail and retry, and persistence reconciliation blocks on that job forever. +func TestESVisibility_Tool_ShutdownFooterReachableOnFailure(t *testing.T) { + b := esBuilder("1.30.5") + b.instance.Spec.MTLS = &v1beta1.MTLSSpec{Provider: v1beta1.LinkerdMTLSProvider} + store := esVisibilityStore() + + for name, script := range map[string]func() (string, error){ + "setup": func() (string, error) { return b.GetStoreSetupTemplate(store) }, + "update": func() (string, error) { return b.GetStoreUpdateTemplate(store, VisibilitySchema) }, + } { + t.Run(name, func(t *testing.T) { + rendered, err := script() + require.NoError(t, err) + + assert.NotRegexp(t, `(?m)^\s*set -e`, rendered, + "set -e would skip the sidecar shutdown footer when a step fails") + assert.Contains(t, rendered, "localhost:4191/shutdown", "shutdown footer must be present") + assert.Contains(t, rendered, "exit $x", "the tool's exit status must still be propagated") + // The wget call must not swallow its own status either: both mesh + // providers behave identically here. + assert.NotContains(t, rendered, "|| true") + }) + } +} + +// TestESVisibility_Tool_StepsFailFast asserts the steps are chained so a failed +// setup-schema does not let create-index run (and report success) anyway. +func TestESVisibility_Tool_StepsFailFast(t *testing.T) { + b := esBuilder("1.30.5") + store := esVisibilityStore() + store.Elasticsearch.Indices.SecondaryVisibility = "temporal_visibility_v1_dev_secondary" + + setup, err := b.GetStoreSetupTemplate(store) + require.NoError(t, err) + assert.Contains(t, setup, "setup-schema && \\") + assert.Contains(t, setup, `create-index --index "temporal_visibility_v1_dev" && \`) + assert.Contains(t, setup, `create-index --index "temporal_visibility_v1_dev_secondary"`) +} diff --git a/internal/resource/persistence/schema_setup_job_builder.go b/internal/resource/persistence/schema_setup_job_builder.go index 1d504c41..21e98745 100644 --- a/internal/resource/persistence/schema_setup_job_builder.go +++ b/internal/resource/persistence/schema_setup_job_builder.go @@ -62,38 +62,36 @@ func (b *SchemaJobBuilder) Enabled() bool { func (b *SchemaJobBuilder) Build() client.Object { datastores := b.instance.Spec.Persistence.GetDatastores() - envVars := []corev1.EnvVar{ - { - Name: "TEMPORAL_CLI_ADDRESS", - Value: fmt.Sprintf("%s:%d", b.instance.ChildResourceName("frontend"), *b.instance.Spec.Services.Frontend.Port), - }, - } - envVars = append(envVars, GetDatastoresEnvironmentVariables(datastores)...) - - volumeMounts := []corev1.VolumeMount{ - { - Name: "scripts", - MountPath: "/etc/scripts", - }, - } + datastoreEnvVars := GetDatastoresEnvironmentVariables(datastores) + envVars := make([]corev1.EnvVar, 0, 1+len(datastoreEnvVars)) + envVars = append(envVars, corev1.EnvVar{ + Name: "TEMPORAL_CLI_ADDRESS", + Value: fmt.Sprintf("%s:%d", b.instance.ChildResourceName("frontend"), *b.instance.Spec.Services.Frontend.Port), + }) + envVars = append(envVars, datastoreEnvVars...) - volumeMounts = append(volumeMounts, GetDatastoresVolumeMounts(datastores)...) + datastoreVolumeMounts := GetDatastoresVolumeMounts(datastores) + volumeMounts := make([]corev1.VolumeMount, 0, 1+len(datastoreVolumeMounts)) + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: "scripts", + MountPath: "/etc/scripts", + }) + volumeMounts = append(volumeMounts, datastoreVolumeMounts...) - volumes := []corev1.Volume{ - { - Name: "scripts", - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: b.instance.ChildResourceName("schema-scripts"), - }, - DefaultMode: ptr.To[int32](0o777), + datastoreVolumes := GetDatastoresVolumes(datastores) + volumes := make([]corev1.Volume, 0, 1+len(datastoreVolumes)) + volumes = append(volumes, corev1.Volume{ + Name: "scripts", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: b.instance.ChildResourceName("schema-scripts"), }, + DefaultMode: ptr.To[int32](0o777), }, }, - } - - volumes = append(volumes, GetDatastoresVolumes(datastores)...) + }) + volumes = append(volumes, datastoreVolumes...) return &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ diff --git a/internal/resource/persistence/template.go b/internal/resource/persistence/template.go index fea01c75..a8597a81 100644 --- a/internal/resource/persistence/template.go +++ b/internal/resource/persistence/template.go @@ -34,10 +34,15 @@ const ( // Setup schemas templates. setupSchemaTemplate = "setup-schema.sh" setupESVisibility = "setup-es-visibility.sh" + // setupESVisibilityTool uses temporal-elasticsearch-tool (Temporal >= 1.30, + // where curl/jq were removed from the admin-tools image). + setupESVisibilityTool = "setup-es-visibility-tool.sh" // Update schemas templates. updateSchemaTemplate = "update-schema.sh" updateESVisibility = "update-es-visibility.sh" + // updateESVisibilityTool uses temporal-elasticsearch-tool (Temporal >= 1.30). + updateESVisibilityTool = "update-es-visibility-tool.sh" // noOpTemplate does nothing. noOpTemplate = "no-op.sh" @@ -90,6 +95,24 @@ var ( curl --user "{{ .Username }}":"${{ .PasswordEnvVar }}" -X PUT "{{ .URL }}/{{ .Indices.SecondaryVisibility }}" --write-out "\n" {{ end }} {{ template "scripts" . }} + `), + // setupESVisibilityTool targets Temporal >= 1.30, whose admin-tools image + // dropped curl/jq and ships temporal-elasticsearch-tool instead. setup-schema + // applies the (embedded) cluster settings + index template; create-index + // creates the visibility index (idempotent: the tool treats + // resource_already_exists_exception as success, so the job is re-runnable). + // + // The steps are chained with && rather than guarded by "set -e": the + // shared "scripts" footer must still run when a step fails, otherwise the + // service-mesh sidecar is never told to shut down and the Job pod stays + // Running forever instead of failing. && gives the same fail-fast + // behaviour while leaving $? for the footer to propagate. + setupESVisibilityTool: dedent.Dedent(` + #!/bin/sh + {{ .Tool }} {{ .ConnectionArgs }} setup-schema && \ + {{ .Tool }} {{ .ConnectionArgs }} create-index --index "{{ .Indices.Visibility }}"{{ if .Indices.SecondaryVisibility }} && \ + {{ .Tool }} {{ .ConnectionArgs }} create-index --index "{{ .Indices.SecondaryVisibility }}"{{ end }} + {{ template "scripts" . }} `), updateESVisibility: dedent.Dedent(` #!/bin/bash @@ -381,6 +404,17 @@ var ( sleep 1 done {{ template "scripts" . }} + `), + // updateESVisibilityTool targets Temporal >= 1.30. update-schema upgrades the + // index template to the version embedded in the tool, and the per-index + // mappings when --index is given (covers all built-in search attributes). + // Chained with && rather than "set -e" so the shared "scripts" footer still + // runs on failure; see setupESVisibilityTool. + updateESVisibilityTool: dedent.Dedent(` + #!/bin/sh + {{ .Tool }} {{ .ConnectionArgs }} update-schema --index "{{ .Indices.Visibility }}"{{ if .Indices.SecondaryVisibility }} && \ + {{ .Tool }} {{ .ConnectionArgs }} update-schema --index "{{ .Indices.SecondaryVisibility }}"{{ end }} + {{ template "scripts" . }} `), } ) @@ -388,6 +422,9 @@ var ( type ( baseData struct { MTLSProvider string + // UseWget makes the MTLS sidecar-shutdown footer use busybox wget instead + // of curl, which was removed from the admin-tools image in Temporal >= 1.30. + UseWget bool } createDatabase struct { @@ -426,18 +463,36 @@ type ( PasswordEnvVar string Indices v1beta1.ElasticsearchIndices } + + // esToolData drives the temporal-elasticsearch-tool based templates (Temporal >= 1.30). + esToolData struct { + baseData + Tool string + ConnectionArgs string + Indices v1beta1.ElasticsearchIndices + } ) +// proxyShutdownScriptsContent tells the service-mesh sidecar to quit once the +// script's real work is done, so the Job pod can terminate. +// +// The exit status is captured into $x *before* the shutdown call, so the +// shutdown command's own status never masks the script's. No "|| true" is +// needed for that, and adding one only hides a genuinely unreachable proxy — +// the wget and curl branches deliberately behave identically here. +// +// wget is BusyBox wget (verified: admin-tools 1.31.1 ships BusyBox v1.37.0 and +// no curl at all); it supports --post-data. var proxyShutdownScriptsContent = dedent.Dedent(` {{- define "scripts" -}} {{- if eq .MTLSProvider "linkerd" -}} x=$? - curl -X POST http://localhost:4191/shutdown + {{ if .UseWget }}wget -q -O- --post-data='' http://localhost:4191/shutdown{{ else }}curl -X POST http://localhost:4191/shutdown{{ end }} exit $x {{- end -}} {{- if eq .MTLSProvider "istio" -}} x=$? - curl -sf -XPOST http://127.0.0.1:15020/quitquitquit + {{ if .UseWget }}wget -q -O- --post-data='' http://127.0.0.1:15020/quitquitquit{{ else }}curl -sf -XPOST http://127.0.0.1:15020/quitquitquit{{ end }} exit $x {{- end -}} {{- end -}} diff --git a/internal/resource/persistence/template_test.go b/internal/resource/persistence/template_test.go index 6cb8f662..89b36694 100644 --- a/internal/resource/persistence/template_test.go +++ b/internal/resource/persistence/template_test.go @@ -25,14 +25,22 @@ import ( ) func TestTemplates(t *testing.T) { - var s strings.Builder - assert.NoError(t, templates[createDatabaseTemplate].Execute(&s, struct { + type data struct { MTLSProvider string + UseWget bool Tool string ConnectionArgs string DatabaseName string - }{ - MTLSProvider: "linkerd", - })) + } + + // Without UseWget (Temporal < 1.30), the linkerd shutdown uses curl. + var s strings.Builder + assert.NoError(t, templates[createDatabaseTemplate].Execute(&s, data{MTLSProvider: "linkerd"})) assert.Contains(t, s.String(), "curl -X POST http://localhost:4191/shutdown") + + // With UseWget (Temporal >= 1.30, curl removed from the image), it uses busybox wget. + var w strings.Builder + assert.NoError(t, templates[createDatabaseTemplate].Execute(&w, data{MTLSProvider: "linkerd", UseWget: true})) + assert.Contains(t, w.String(), "wget -q -O- --post-data='' http://localhost:4191/shutdown") + assert.NotContains(t, w.String(), "curl") } diff --git a/internal/resource/ui/deployment_builder.go b/internal/resource/ui/deployment_builder.go index a5dbec42..44de3ec5 100644 --- a/internal/resource/ui/deployment_builder.go +++ b/internal/resource/ui/deployment_builder.go @@ -121,7 +121,7 @@ func (b *DeploymentBuilder) Update(object client.Object) error { MatchLabels: metadata.LabelsSelector(b.instance, "ui"), } deployment.Spec.Template = corev1.PodTemplateSpec{ - ObjectMeta: meta.BuildPodObjectMeta(b.instance, "ui", b.configHash), + ObjectMeta: meta.BuildPodObjectMeta(b.instance, "ui", b.configHash, deployment.Spec.Template.ObjectMeta), Spec: corev1.PodSpec{ ImagePullSecrets: b.instance.Spec.ImagePullSecrets, Containers: []corev1.Container{ diff --git a/main.go b/main.go index 4f2b596d..e2c9b3e4 100644 --- a/main.go +++ b/main.go @@ -108,7 +108,12 @@ func main() { } if err = (&controllers.TemporalClusterReconciler{ - Base: controllers.New(mgr.GetClient(), mgr.GetScheme(), mgr.GetEventRecorderFor("cluster-controller"), discoveryManager), + // GetEventRecorderFor is deprecated in controller-runtime v0.23 in favour of + // GetEventRecorder, but the replacement returns events.EventRecorder rather + // than record.EventRecorder. Those are different interfaces and switching + // moves event emission from the core v1 API group to events.k8s.io/v1, which + // is an observable behaviour change. Deferred to its own change. + Base: controllers.New(mgr.GetClient(), mgr.GetScheme(), mgr.GetEventRecorderFor("cluster-controller"), discoveryManager), //nolint:staticcheck // SA1019: see note above. AvailableAPIs: availableAPIs, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Cluster") @@ -123,7 +128,12 @@ func main() { } if err = (&controllers.TemporalClusterClientReconciler{ - Base: controllers.New(mgr.GetClient(), mgr.GetScheme(), mgr.GetEventRecorderFor("clusterclient-controller"), discoveryManager), + // GetEventRecorderFor is deprecated in controller-runtime v0.23 in favour of + // GetEventRecorder, but the replacement returns events.EventRecorder rather + // than record.EventRecorder. Those are different interfaces and switching + // moves event emission from the core v1 API group to events.k8s.io/v1, which + // is an observable behaviour change. Deferred to its own change. + Base: controllers.New(mgr.GetClient(), mgr.GetScheme(), mgr.GetEventRecorderFor("clusterclient-controller"), discoveryManager), //nolint:staticcheck // SA1019: see note above. AvailableAPIs: availableAPIs, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "ClusterClient") diff --git a/pkg/kubernetes/overrides.go b/pkg/kubernetes/overrides.go index 12c908c3..94f02b92 100644 --- a/pkg/kubernetes/overrides.go +++ b/pkg/kubernetes/overrides.go @@ -83,7 +83,17 @@ func ApplyPodTemplateSpecOverrides(podTemplate *corev1.PodTemplateSpec, override if err != nil { return fmt.Errorf("can't patch pod template spec: %w", err) } - return json.Unmarshal(patched, &podTemplate.Spec) + + // Unmarshal into a zeroed spec rather than the still-populated one: + // json.Unmarshal merges JSON arrays element-wise into existing slice + // elements, so patched list entries would inherit leftover fields + // (e.g. an env var ending up with both value and valueFrom). + patchedSpec := corev1.PodSpec{} + err = json.Unmarshal(patched, &patchedSpec) + if err != nil { + return fmt.Errorf("can't unmarshal patched pod template spec: %w", err) + } + podTemplate.Spec = patchedSpec } return nil } @@ -126,7 +136,16 @@ func ApplyDeploymentOverrides(deployment *appsv1.Deployment, override *v1beta1.D if err != nil { return fmt.Errorf("can't apply json patch: %w", err) } - return json.Unmarshal(patched, &deployment) + + // Unmarshal into a zeroed deployment for the same reason as above: + // decoding into the populated one would merge patched list elements + // with leftover fields from the previous elements. + patchedDeployment := appsv1.Deployment{} + err = json.Unmarshal(patched, &patchedDeployment) + if err != nil { + return fmt.Errorf("can't unmarshal patched deployment: %w", err) + } + *deployment = patchedDeployment } return nil diff --git a/pkg/kubernetes/overrides_test.go b/pkg/kubernetes/overrides_test.go index c81d6dc9..b818b38e 100644 --- a/pkg/kubernetes/overrides_test.go +++ b/pkg/kubernetes/overrides_test.go @@ -498,6 +498,259 @@ func TestApplyDeploymentOverrides(t *testing.T) { }, }, }, + "add env var to existing env using strategic merge": { + original: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "service", + Env: []corev1.EnvVar{ + { + Name: "POD_IP", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "status.podIP", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + override: &v1beta1.DeploymentOverride{ + Spec: &v1beta1.DeploymentOverrideSpec{ + Template: &v1beta1.PodTemplateSpecOverride{ + Spec: &apiextensionsv1.JSON{ + Raw: []byte(`{"containers":[{"name":"service","env":[{"name":"MY_VAR","value":"my-value"}]}]}`), + }, + }, + }, + }, + expected: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "service", + Env: []corev1.EnvVar{ + { + Name: "MY_VAR", + Value: "my-value", + }, + { + Name: "POD_IP", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "status.podIP", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "add secret volume to existing volumes using strategic merge": { + original: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "service", + }, + }, + Volumes: []corev1.Volume{ + { + Name: "config", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "test", + }, + }, + }, + }, + }, + }, + }, + }, + }, + override: &v1beta1.DeploymentOverride{ + Spec: &v1beta1.DeploymentOverrideSpec{ + Template: &v1beta1.PodTemplateSpecOverride{ + Spec: &apiextensionsv1.JSON{ + Raw: []byte(`{"volumes":[{"name":"secrets","secret":{"secretName":"my-secret"}}]}`), + }, + }, + }, + }, + expected: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "service", + }, + }, + Volumes: []corev1.Volume{ + { + Name: "secrets", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "my-secret", + }, + }, + }, + { + Name: "config", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "test", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "remove env var using json patch": { + original: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "service", + Env: []corev1.EnvVar{ + { + Name: "POD_IP", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "status.podIP", + }, + }, + }, + { + Name: "MY_VAR", + Value: "my-value", + }, + }, + }, + }, + }, + }, + }, + }, + override: &v1beta1.DeploymentOverride{ + JSONPatch: &apiextensionsv1.JSON{ + Raw: []byte(`[{"op":"remove", "path":"/spec/template/spec/containers/0/env/0"}]`), + }, + }, + expected: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "service", + Env: []corev1.EnvVar{ + { + Name: "MY_VAR", + Value: "my-value", + }, + }, + }, + }, + }, + }, + }, + }, + }, + "merge pod template labels and annotations": { + original: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "a": "b", + }, + Annotations: map[string]string{ + "c": "d", + }, + }, + }, + }, + }, + override: &v1beta1.DeploymentOverride{ + Spec: &v1beta1.DeploymentOverrideSpec{ + Template: &v1beta1.PodTemplateSpecOverride{ + ObjectMetaOverride: &v1beta1.ObjectMetaOverride{ + Labels: map[string]string{ + "e": "f", + }, + Annotations: map[string]string{ + "g": "h", + }, + }, + }, + }, + }, + expected: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "a": "b", + "e": "f", + }, + Annotations: map[string]string{ + "c": "d", + "g": "h", + }, + }, + }, + }, + }, + }, } for name, test := range tests { diff --git a/pkg/temporal/client.go b/pkg/temporal/client.go index 797bf9f6..5ea1260c 100644 --- a/pkg/temporal/client.go +++ b/pkg/temporal/client.go @@ -69,13 +69,14 @@ func GetTlSConfigFromSecret(secret *corev1.Secret) (*tls.Config, error) { }, nil } -// GetClusterClientTLSConfig returns the tls configuration for the provided temporal cluster. -func GetClusterClientTLSConfig(ctx context.Context, client client.Client, cluster *v1beta1.TemporalCluster) (*tls.Config, error) { +// getClientTLSConfig returns a client tls configuration using the certificate +// held in the provided secret and the provided server name. +func getClientTLSConfig(ctx context.Context, client client.Client, namespace, secretName, serverName string) (*tls.Config, error) { secret := &corev1.Secret{} err := client.Get(ctx, types.NamespacedName{ - Name: cluster.ChildResourceName(certmanager.FrontendCertificate), - Namespace: cluster.GetNamespace(), + Name: secretName, + Namespace: namespace, }, secret) if err != nil { return nil, err @@ -86,16 +87,44 @@ func GetClusterClientTLSConfig(ctx context.Context, client client.Client, cluste return nil, err } - tlsConfig.ServerName = cluster.Spec.MTLS.Frontend.ServerName(cluster) + tlsConfig.ServerName = serverName return tlsConfig, nil } +// GetClusterClientTLSConfig returns the tls configuration for the provided temporal cluster. +func GetClusterClientTLSConfig(ctx context.Context, client client.Client, cluster *v1beta1.TemporalCluster) (*tls.Config, error) { + return getClientTLSConfig(ctx, client, + cluster.GetNamespace(), + cluster.ChildResourceName(certmanager.FrontendCertificate), + cluster.Spec.MTLS.Frontend.ServerName(cluster), + ) +} + func buildClusterClientOptions(ctx context.Context, client client.Client, cluster *v1beta1.TemporalCluster, overrides ...ClientOption) (temporalclient.Options, error) { opts := temporalclient.Options{ - HostPort: cluster.GetPublicClientAddress(), + HostPort: cluster.GetOperatorClientAddress(), Logger: temporallog.NewTemporalSDKLogFromContext(ctx), } - if cluster.MTLSWithCertManagerEnabled() && cluster.Spec.MTLS.FrontendEnabled() { + + internalFrontendEnabled := cluster.Spec.Services != nil && cluster.Spec.Services.InternalFrontend.IsEnabled() + + switch { + case internalFrontendEnabled && cluster.MTLSWithCertManagerEnabled() && cluster.Spec.MTLS.InternodeEnabled(): + // The internal frontend is served using the internode mTLS settings, + // so authenticate using the internode certificate. + tlsConfig, err := getClientTLSConfig(ctx, client, + cluster.GetNamespace(), + cluster.ChildResourceName(certmanager.InternodeCertificate), + cluster.Spec.MTLS.Internode.ServerName(cluster), + ) + if err != nil { + return opts, fmt.Errorf("can't get cluster TLS config: %w", err) + } + opts.ConnectionOptions.TLS = tlsConfig + case internalFrontendEnabled: + // The internal frontend serves plaintext when internode mTLS is disabled, + // even if frontend mTLS is enabled. + case cluster.MTLSWithCertManagerEnabled() && cluster.Spec.MTLS.FrontendEnabled(): tlsConfig, err := GetClusterClientTLSConfig(ctx, client, cluster) if err != nil { return opts, fmt.Errorf("can't get cluster TLS config: %w", err) diff --git a/pkg/temporal/client_test.go b/pkg/temporal/client_test.go new file mode 100644 index 00000000..295c2172 --- /dev/null +++ b/pkg/temporal/client_test.go @@ -0,0 +1,332 @@ +// Licensed to Alexandre VILAIN under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Alexandre VILAIN licenses this file to you 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 temporal + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "testing" + "time" + + "github.com/alexandrevilain/temporal-operator/api/v1beta1" + "github.com/alexandrevilain/temporal-operator/internal/resource/mtls/certmanager" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +// newTLSSecretData returns valid secret data for GetTlSConfigFromSecret, +// holding a self-signed certificate whose CommonName is the provided name. +func newTLSSecretData(t *testing.T, commonName string) map[string][]byte { + t.Helper() + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: commonName}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + } + + certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + require.NoError(t, err) + + keyDER, err := x509.MarshalECPrivateKey(key) + require.NoError(t, err) + + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}) + + return map[string][]byte{ + certmanager.TLSCA: certPEM, + certmanager.TLSCert: certPEM, + certmanager.TLSKey: keyPEM, + } +} + +func newTLSSecret(t *testing.T, name string) *corev1.Secret { + t.Helper() + + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "default", + }, + Data: newTLSSecretData(t, name), + } +} + +func newFakeTemporalCluster(frontendMTLS, internodeMTLS, internalFrontend bool) *v1beta1.TemporalCluster { + cluster := &v1beta1.TemporalCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake", + Namespace: "default", + }, + Spec: v1beta1.TemporalClusterSpec{ + Services: &v1beta1.ServicesSpec{ + Frontend: &v1beta1.ServiceSpec{ + Port: ptr.To[int32](7233), + }, + }, + }, + } + + if internalFrontend { + cluster.Spec.Services.InternalFrontend = &v1beta1.InternalFrontendServiceSpec{ + ServiceSpec: v1beta1.ServiceSpec{ + Port: ptr.To[int32](7236), + }, + Enabled: true, + } + } + + if frontendMTLS || internodeMTLS { + cluster.Spec.MTLS = &v1beta1.MTLSSpec{ + Provider: v1beta1.CertManagerMTLSProvider, + } + if frontendMTLS { + cluster.Spec.MTLS.Frontend = &v1beta1.FrontendMTLSSpec{Enabled: true} + } + if internodeMTLS { + cluster.Spec.MTLS.Internode = &v1beta1.InternodeMTLSSpec{Enabled: true} + } + } + + return cluster +} + +// clientCertificateCommonName returns the CommonName of the client certificate +// held in the provided tls config, proving which secret it was loaded from. +func clientCertificateCommonName(t *testing.T, cfg *tls.Config) string { + t.Helper() + + require.Len(t, cfg.Certificates, 1) + require.NotEmpty(t, cfg.Certificates[0].Certificate) + + cert, err := x509.ParseCertificate(cfg.Certificates[0].Certificate[0]) + require.NoError(t, err) + + return cert.Subject.CommonName +} + +func TestBuildClusterClientOptions(t *testing.T) { + frontendAddress := "fake-frontend.default:7233" + internalFrontendAddress := "fake-internal-frontend-headless.default:7236" + + tests := map[string]struct { + frontendMTLS bool + internodeMTLS bool + internalFrontend bool + expectedHostPort string + expectedTLS bool + expectedServerName string + expectedCertName string + }{ + "no mTLS, no internal frontend": { + expectedHostPort: frontendAddress, + }, + "frontend mTLS, no internal frontend": { + frontendMTLS: true, + expectedHostPort: frontendAddress, + expectedTLS: true, + expectedServerName: "fake-frontend.default.svc.cluster.local", + expectedCertName: "fake-frontend-certificate", + }, + "internode mTLS, no internal frontend": { + internodeMTLS: true, + expectedHostPort: frontendAddress, + }, + "frontend and internode mTLS, no internal frontend": { + frontendMTLS: true, + internodeMTLS: true, + expectedHostPort: frontendAddress, + expectedTLS: true, + expectedServerName: "fake-frontend.default.svc.cluster.local", + expectedCertName: "fake-frontend-certificate", + }, + "no mTLS, internal frontend": { + internalFrontend: true, + expectedHostPort: internalFrontendAddress, + }, + "frontend mTLS, internal frontend": { + // The internal frontend serves plaintext when internode mTLS + // is disabled, even if frontend mTLS is enabled. + frontendMTLS: true, + internalFrontend: true, + expectedHostPort: internalFrontendAddress, + }, + "internode mTLS, internal frontend": { + internodeMTLS: true, + internalFrontend: true, + expectedHostPort: internalFrontendAddress, + expectedTLS: true, + expectedServerName: "fake-internode.default.svc.cluster.local", + expectedCertName: "fake-internode-certificate", + }, + "frontend and internode mTLS, internal frontend": { + frontendMTLS: true, + internodeMTLS: true, + internalFrontend: true, + expectedHostPort: internalFrontendAddress, + expectedTLS: true, + expectedServerName: "fake-internode.default.svc.cluster.local", + expectedCertName: "fake-internode-certificate", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + ctx := context.Background() + cluster := newFakeTemporalCluster(test.frontendMTLS, test.internodeMTLS, test.internalFrontend) + + fakeClient := fake.NewClientBuilder(). + WithObjects( + newTLSSecret(t, "fake-frontend-certificate"), + newTLSSecret(t, "fake-internode-certificate"), + ). + Build() + + opts, err := buildClusterClientOptions(ctx, fakeClient, cluster) + require.NoError(t, err) + + assert.Equal(t, test.expectedHostPort, opts.HostPort) + + if !test.expectedTLS { + assert.Nil(t, opts.ConnectionOptions.TLS) + return + } + + require.NotNil(t, opts.ConnectionOptions.TLS) + assert.Equal(t, test.expectedServerName, opts.ConnectionOptions.TLS.ServerName) + assert.NotNil(t, opts.ConnectionOptions.TLS.RootCAs) + assert.Equal(t, test.expectedCertName, clientCertificateCommonName(t, opts.ConnectionOptions.TLS)) + }) + } +} + +func TestBuildClusterClientOptionsErrors(t *testing.T) { + tests := map[string]struct { + frontendMTLS bool + internodeMTLS bool + internalFrontend bool + secrets func(t *testing.T) []client.Object + expectedError string + }{ + "frontend certificate secret not found": { + frontendMTLS: true, + secrets: func(*testing.T) []client.Object { + return nil + }, + expectedError: "fake-frontend-certificate", + }, + "internode certificate secret not found": { + internodeMTLS: true, + internalFrontend: true, + secrets: func(*testing.T) []client.Object { + return nil + }, + expectedError: "fake-internode-certificate", + }, + "secret misses ca.crt": { + frontendMTLS: true, + secrets: func(t *testing.T) []client.Object { + t.Helper() + secret := newTLSSecret(t, "fake-frontend-certificate") + delete(secret.Data, certmanager.TLSCA) + return []client.Object{secret} + }, + expectedError: "can't get ca.crt from client secret", + }, + "secret misses tls.crt": { + internodeMTLS: true, + internalFrontend: true, + secrets: func(t *testing.T) []client.Object { + t.Helper() + secret := newTLSSecret(t, "fake-internode-certificate") + delete(secret.Data, certmanager.TLSCert) + return []client.Object{secret} + }, + expectedError: "can't get tls.crt from client secret", + }, + "secret misses tls.key": { + internodeMTLS: true, + internalFrontend: true, + secrets: func(t *testing.T) []client.Object { + t.Helper() + secret := newTLSSecret(t, "fake-internode-certificate") + delete(secret.Data, certmanager.TLSKey) + return []client.Object{secret} + }, + expectedError: "can't get tls.key from client secret", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + ctx := context.Background() + cluster := newFakeTemporalCluster(test.frontendMTLS, test.internodeMTLS, test.internalFrontend) + + fakeClient := fake.NewClientBuilder(). + WithObjects(test.secrets(t)...). + Build() + + _, err := buildClusterClientOptions(ctx, fakeClient, cluster) + require.Error(t, err) + assert.ErrorContains(t, err, test.expectedError) + }) + } +} + +func TestBuildClusterClientOptionsOverrides(t *testing.T) { + ctx := context.Background() + cluster := newFakeTemporalCluster(true, true, true) + + fakeClient := fake.NewClientBuilder(). + WithObjects( + newTLSSecret(t, "fake-frontend-certificate"), + newTLSSecret(t, "fake-internode-certificate"), + ). + Build() + + overrideTLSConfig := &tls.Config{ServerName: "override.example.com", MinVersion: tls.VersionTLS12} + + opts, err := buildClusterClientOptions(ctx, fakeClient, cluster, + WithHostPort("override.example.com:7233"), + WithTLSConfig(overrideTLSConfig), + ) + require.NoError(t, err) + + assert.Equal(t, "override.example.com:7233", opts.HostPort) + assert.Same(t, overrideTLSConfig, opts.ConnectionOptions.TLS) +} diff --git a/pkg/temporal/config/dynamicconfig.go b/pkg/temporal/config/dynamicconfig.go index dc388eb8..3231005b 100644 --- a/pkg/temporal/config/dynamicconfig.go +++ b/pkg/temporal/config/dynamicconfig.go @@ -18,7 +18,9 @@ package config import ( + "bytes" "encoding/json" + "math" "github.com/alexandrevilain/temporal-operator/api/v1beta1" ) @@ -80,14 +82,86 @@ func constrainedValueToYamlConstrainedValue(cv *v1beta1.ConstrainedValue) (YamlC constraints["shardid"] = cv.Constraints.ShardID } + // Decode the raw JSON value using a decoder with UseNumber so that JSON + // numbers are preserved as json.Number instead of being coerced to float64. + // Without this, an integer like 2097152 becomes float64(2097152), which + // yaml.v3 later marshals in scientific notation (2.097152e+06). Temporal's + // file based dynamic config client then fails to parse it for settings that + // expect an integer. + decoder := json.NewDecoder(bytes.NewReader(cv.Value.Raw)) + decoder.UseNumber() + var value any - err := json.Unmarshal(cv.Value.Raw, &value) + err := decoder.Decode(&value) if err != nil { return YamlConstrainedValue{}, err } return YamlConstrainedValue{ Constraints: constraints, - Value: value, + Value: normalizeJSONNumbers(value), }, nil } + +// normalizeJSONNumbers recursively walks a value decoded from JSON with +// json.Decoder.UseNumber and converts every json.Number into a concrete int or +// float64. Integers are converted to int to match the type yaml.v3 produces +// when it unmarshals the config map back, keeping the reconciliation deep-equal +// comparison stable. +func normalizeJSONNumbers(value any) any { + switch v := value.(type) { + case json.Number: + return normalizeJSONNumber(v) + case map[string]any: + for key, val := range v { + v[key] = normalizeJSONNumbers(val) + } + return v + case []any: + for i, val := range v { + v[i] = normalizeJSONNumbers(val) + } + return v + default: + return value + } +} + +// normalizeJSONNumber converts a single json.Number into the Go numeric type +// that yaml.v3 round-trips without changing its textual form. +func normalizeJSONNumber(n json.Number) any { + if i, err := n.Int64(); err == nil { + return narrowInt(i) + } + + if f, err := n.Float64(); err == nil { + // Exponent notation such as 1e9 is valid JSON and is an integer, but + // json.Number.Int64 cannot parse it. Falling through to float64 here + // would make yaml.v3 write it back as "1e+09", which Temporal's + // file-based dynamic config client rejects for settings that expect an + // integer -- precisely the failure this normalisation exists to avoid. + // So integral values are converted back to an integer type. Note that + // returning n.String() instead would emit a quoted YAML string, which + // Temporal rejects just the same. + if f == math.Trunc(f) && f >= float64(math.MinInt64) && f < float64(math.MaxInt64) { + return narrowInt(int64(f)) + } + return f + } + + // Not representable as either (e.g. more precision than float64 holds). + // Keep the original text rather than silently losing digits. + return n.String() +} + +// narrowInt returns i as an int when that is lossless, matching the type +// yaml.v3 produces when it unmarshals the rendered config map back. Keeping the +// types identical is what makes the reconciliation deep-equal comparison +// stable. On platforms where int is 32 bits, values that do not fit stay int64 +// rather than silently wrapping to a different number. +func narrowInt(i int64) any { + if int64(int(i)) == i { + return int(i) + } + return i +} diff --git a/pkg/temporal/config/dynamicconfig_test.go b/pkg/temporal/config/dynamicconfig_test.go index 35628c3b..b061ed0e 100644 --- a/pkg/temporal/config/dynamicconfig_test.go +++ b/pkg/temporal/config/dynamicconfig_test.go @@ -24,6 +24,7 @@ import ( "github.com/alexandrevilain/temporal-operator/pkg/temporal/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" ) @@ -46,7 +47,7 @@ func TestDynamicConfigToYamlDynamicConfig(t *testing.T) { "matching.numTaskqueueReadPartitions": { { Constraints: map[string]any{}, - Value: float64(5), + Value: int(5), }, }, }, @@ -70,7 +71,7 @@ func TestDynamicConfigToYamlDynamicConfig(t *testing.T) { Constraints: map[string]any{ "namespace": "accounting", }, - Value: float64(5), + Value: int(5), }, }, }, @@ -98,7 +99,7 @@ func TestDynamicConfigToYamlDynamicConfig(t *testing.T) { "taskqueuename": "accounting-tq", "shardid": int32(1), }, - Value: float64(5), + Value: int(5), }, }, }, @@ -122,7 +123,7 @@ func TestDynamicConfigToYamlDynamicConfig(t *testing.T) { Constraints: map[string]any{ "tasktype": "Workflow", }, - Value: float64(5), + Value: int(5), }, }, }, @@ -146,7 +147,7 @@ func TestDynamicConfigToYamlDynamicConfig(t *testing.T) { Constraints: map[string]any{ "historytasktype": "ActivityRetryTimer", }, - Value: float64(5), + Value: int(5), }, }, }, @@ -161,3 +162,108 @@ func TestDynamicConfigToYamlDynamicConfig(t *testing.T) { }) } } + +// TestDynamicConfigToYamlDynamicConfigLargeInteger ensures that large integer +// values are marshaled as plain integers and not in floating-point scientific +// notation (e.g. 2.097152e+06). Temporal's file based dynamic config client +// parses the resulting YAML and fails to load a setting when the number is +// rendered as a float for a setting that expects an integer. +func TestDynamicConfigToYamlDynamicConfigLargeInteger(t *testing.T) { + dc := &v1beta1.DynamicConfigSpec{ + Values: map[string][]v1beta1.ConstrainedValue{ + "limit.blobSize.error": { + { + Value: &apiextensionsv1.JSON{Raw: []byte(`2097152`)}, + }, + }, + }, + } + + result, err := config.DynamicConfigToYamlDynamicConfig(dc) + require.NoError(t, err) + + out, err := yaml.Marshal(result) + require.NoError(t, err) + + assert.Contains(t, string(out), "value: 2097152") + assert.NotContains(t, string(out), "2.097152e+06") +} + +// TestDynamicConfigToYamlDynamicConfigNestedLargeInteger ensures large integers +// nested inside object/array values are also rendered as plain integers, since +// json.Unmarshal into an any coerces every number (including nested ones) to +// float64. +func TestDynamicConfigToYamlDynamicConfigNestedLargeInteger(t *testing.T) { + dc := &v1beta1.DynamicConfigSpec{ + Values: map[string][]v1beta1.ConstrainedValue{ + "history.defaultActivityRetryPolicy": { + { + Value: &apiextensionsv1.JSON{Raw: []byte(`{"MaximumInterval": 2097152, "Sizes": [1048576, 4194304]}`)}, + }, + }, + }, + } + + result, err := config.DynamicConfigToYamlDynamicConfig(dc) + require.NoError(t, err) + + out, err := yaml.Marshal(result) + require.NoError(t, err) + + assert.Contains(t, string(out), "MaximumInterval: 2097152") + assert.Contains(t, string(out), "- 1048576") + assert.Contains(t, string(out), "- 4194304") + assert.NotContains(t, string(out), "e+06") +} + +// TestDynamicConfigToYamlDynamicConfigExponentInteger covers integers written in +// exponent form. They are valid JSON but json.Number.Int64 cannot parse them, so +// they used to fall through to float64 and be re-emitted as "1e+09" — the exact +// scientific-notation output this normalisation exists to prevent, which +// Temporal's file-based dynamic config client rejects for an int setting. +func TestDynamicConfigToYamlDynamicConfigExponentInteger(t *testing.T) { + dc := &v1beta1.DynamicConfigSpec{ + Values: map[string][]v1beta1.ConstrainedValue{ + "limit.blobSize.error": { + { + Value: &apiextensionsv1.JSON{Raw: []byte(`1e9`)}, + }, + }, + }, + } + + result, err := config.DynamicConfigToYamlDynamicConfig(dc) + require.NoError(t, err) + + out, err := yaml.Marshal(result) + require.NoError(t, err) + + assert.Contains(t, string(out), "value: 1000000000") + assert.NotContains(t, string(out), "e+09") + // A quoted string would be rejected by Temporal just the same as scientific + // notation, so the value must not be rendered as text either. + assert.NotContains(t, string(out), `"1e9"`) +} + +// TestDynamicConfigToYamlDynamicConfigInt64Value covers a value that exceeds a +// 32-bit int. It must survive intact rather than wrapping, which is what the +// previous unconditional int() conversion did on 32-bit builds. +func TestDynamicConfigToYamlDynamicConfigInt64Value(t *testing.T) { + dc := &v1beta1.DynamicConfigSpec{ + Values: map[string][]v1beta1.ConstrainedValue{ + "limit.blobSize.error": { + { + Value: &apiextensionsv1.JSON{Raw: []byte(`5368709120`)}, + }, + }, + }, + } + + result, err := config.DynamicConfigToYamlDynamicConfig(dc) + require.NoError(t, err) + + out, err := yaml.Marshal(result) + require.NoError(t, err) + + assert.Contains(t, string(out), "value: 5368709120") +} diff --git a/pkg/temporal/persistence/config.go b/pkg/temporal/persistence/config.go index f89ac65f..ea028706 100644 --- a/pkg/temporal/persistence/config.go +++ b/pkg/temporal/persistence/config.go @@ -30,7 +30,7 @@ import ( // NewSQLconfigFromDatastoreSpec creates a new instance of a temporal SQL config from the provided DatastoreSpec. func NewSQLConfigFromDatastoreSpec(spec *v1beta1.DatastoreSpec) *config.SQL { - return &config.SQL{ + cfg := &config.SQL{ User: spec.SQL.User, Password: "", PluginName: spec.SQL.PluginName, @@ -44,6 +44,23 @@ func NewSQLConfigFromDatastoreSpec(spec *v1beta1.DatastoreSpec) *config.SQL { TaskScanPartitions: spec.SQL.TaskScanPartitions, TLS: tlsConfigConfigFromDatastoreSpec(spec), } + + // PasswordCommand (Temporal >= 1.31) resolves the datastore password by + // running an external command, e.g. to fetch a cloud IAM auth token. It is + // mutually exclusive with a static password. + if spec.SQL.PasswordCommand != nil { + cfg.PasswordCommand = &config.PasswordCommandConfig{ + Command: spec.SQL.PasswordCommand.Command, + Args: spec.SQL.PasswordCommand.Args, + } + // Leave Timeout at its zero value when unset so the server applies its + // own default rather than a hard 0s. + if spec.SQL.PasswordCommand.Timeout != nil { + cfg.PasswordCommand.Timeout = spec.SQL.PasswordCommand.Timeout.Duration + } + } + + return cfg } // NewElasticsearchConfigFromDatastoreSpec creates a new instance of a temporal elasticsearch client config from the provided DatastoreSpec. diff --git a/pkg/version/admintools_test.go b/pkg/version/admintools_test.go index b7dc0903..5a9419f0 100644 --- a/pkg/version/admintools_test.go +++ b/pkg/version/admintools_test.go @@ -48,6 +48,24 @@ func TestDefaultAdminToolTag(t *testing.T) { version: version.MustNewVersionFromString("1.26.0"), expected: "1.26", }, + { + name: "Version 1.29.7", + version: version.MustNewVersionFromString("1.29.7"), + expected: "1.29", + }, + { + // 1.30 is where the admin-tools image was stripped to bare alpine + // (no curl/jq) and gained temporal-elasticsearch-tool. The tag + // scheme is unchanged, so the major.minor rule still applies. + name: "Version 1.30.5", + version: version.MustNewVersionFromString("1.30.5"), + expected: "1.30", + }, + { + name: "Version 1.31.1", + version: version.MustNewVersionFromString("1.31.1"), + expected: "1.31", + }, { name: "Version 1.10.0", version: version.MustNewVersionFromString("1.10.0"), diff --git a/pkg/version/version.go b/pkg/version/version.go index e1e552ad..372f4b21 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -22,13 +22,14 @@ import ( "encoding/json" "errors" "fmt" + "sync" "github.com/Masterminds/semver/v3" ) var ( // SupportedVersionsRange holds all supported temporal versions. - SupportedVersionsRange = mustNewConstraint(">= 1.14.0 < 1.29.0") + SupportedVersionsRange = mustNewConstraint(">= 1.14.0 < 1.32.0") ForbiddenBrokenReleases = []*Version{ // v1.21.0 is reported as broken, see: https://github.com/temporalio/temporal/releases/tag/v1.21.0 MustNewVersionFromString("1.21.0"), @@ -38,14 +39,25 @@ var ( MustNewVersionFromString("1.24.0"), // v1.27.0 is reported as broken, see: https://github.com/temporalio/temporal/releases/tag/v1.27.0 MustNewVersionFromString("1.27.0"), + // The releases below are retracted by upstream's own go.mod, see the + // retract block in https://github.com/temporalio/temporal/blob/v1.31.2/go.mod + // None of them has a published GitHub release or container image. + // v1.26.0 was "published accidentally"; use v1.26.2+. + MustNewVersionFromString("1.26.0"), + // v1.26.1 "contains retractions only"; use v1.26.2+. + MustNewVersionFromString("1.26.1"), + // v1.30.0 is retracted; use v1.30.1+. + MustNewVersionFromString("1.30.0"), } - V1_18_0 = MustNewVersionFromString("1.18.0") //nolint:stylecheck,revive - V1_20_0 = MustNewVersionFromString("1.20.0") //nolint:stylecheck,revive - V1_21_0 = MustNewVersionFromString("1.21.0") //nolint:stylecheck,revive - V1_22_0 = MustNewVersionFromString("1.22.0") //nolint:stylecheck,revive - V1_23_0 = MustNewVersionFromString("1.23.0") //nolint:stylecheck,revive - V1_24_0 = MustNewVersionFromString("1.24.0") //nolint:stylecheck,revive - V1_25_0 = MustNewVersionFromString("1.25.0") //nolint:stylecheck,revive + V1_18_0 = MustNewVersionFromString("1.18.0") //nolint:staticcheck,revive + V1_20_0 = MustNewVersionFromString("1.20.0") //nolint:staticcheck,revive + V1_21_0 = MustNewVersionFromString("1.21.0") //nolint:staticcheck,revive + V1_22_0 = MustNewVersionFromString("1.22.0") //nolint:staticcheck,revive + V1_23_0 = MustNewVersionFromString("1.23.0") //nolint:staticcheck,revive + V1_24_0 = MustNewVersionFromString("1.24.0") //nolint:staticcheck,revive + V1_25_0 = MustNewVersionFromString("1.25.0") //nolint:staticcheck,revive + V1_30_0 = MustNewVersionFromString("1.30.0") //nolint:staticcheck,revive + V1_31_0 = MustNewVersionFromString("1.31.0") //nolint:staticcheck,revive ) // Version is a wrapper around semver.Version which supports correct @@ -55,6 +67,37 @@ type Version struct { *semver.Version } +// IsBrokenRelease reports whether v is one of the releases the operator refuses +// to run. +func IsBrokenRelease(v *Version) bool { + for _, broken := range ForbiddenBrokenReleases { + if v.Equal(broken.Version) { + return true + } + } + return false +} + +// NextNonBrokenPatch returns the first patch release after v that is not itself +// marked broken. +// +// Broken releases can be consecutive: v1.26.0 and v1.26.1 are both retracted +// upstream. Suggesting a plain IncPatch would send users from 1.26.0 to 1.26.1, +// which the webhook rejects in turn. +func (v *Version) NextNonBrokenPatch() *Version { + candidate := v + // ForbiddenBrokenReleases is finite so this always terminates; the bound is + // only a guard against a future list that fails to advance. + for range len(ForbiddenBrokenReleases) + 1 { + next := candidate.IncPatch() + candidate = &Version{Version: &next} + if !IsBrokenRelease(candidate) { + break + } + } + return candidate +} + // Validate checks if the current version is in the supported temporal cluster // version range. func (v *Version) Validate() error { @@ -99,15 +142,39 @@ func (v Version) MarshalJSON() ([]byte, error) { // GreaterOrEqual returns whenever version is greater or equal than the provided version. func (v *Version) GreaterOrEqual(compare *Version) bool { - str := fmt.Sprintf(">= %s", compare.String()) - c, _ := semver.NewConstraint(str) - return c.Check(v.Version) + return checkConstraint(v, ">= %s", compare) } // LessThan returns whenever version is less than the provided version. func (v *Version) LessThan(compare *Version) bool { - str := fmt.Sprintf("< %s", compare.String()) - c, _ := semver.NewConstraint(str) + return checkConstraint(v, "< %s", compare) +} + +// constraintCache memoizes compiled semver constraints, keyed by their textual +// form. +// +// GreaterOrEqual and LessThan are called repeatedly during a single reconcile — +// once per datastore, once per deployment builder, once per config section — +// and nearly always against the same handful of package-level version +// constants, so recompiling the identical constraint every time is pure waste. +var constraintCache sync.Map + +func checkConstraint(v *Version, format string, compare *Version) bool { + expr := fmt.Sprintf(format, compare.String()) + + if cached, ok := constraintCache.Load(expr); ok { + return cached.(*semver.Constraints).Check(v.Version) + } + + c, err := semver.NewConstraint(expr) + if err != nil { + // compare always renders as a valid semver, so this is unreachable. + // It is handled anyway because the previous code discarded the error + // and would have dereferenced a nil *Constraints if that ever changed. + return false + } + + constraintCache.Store(expr, c) return c.Check(v.Version) } diff --git a/pkg/version/version_test.go b/pkg/version/version_test.go index 1d2dcec7..5ae7252c 100644 --- a/pkg/version/version_test.go +++ b/pkg/version/version_test.go @@ -128,3 +128,32 @@ func TestVersionLessThan(t *testing.T) { }) } } + +// TestNextNonBrokenPatch asserts the upgrade suggestion never points at another +// release the webhook also rejects. v1.26.0 and v1.26.1 are both retracted +// upstream, so a plain IncPatch would send a user from one rejected version +// straight into the next. +func TestNextNonBrokenPatch(t *testing.T) { + for _, tt := range []struct { + from string + want string + }{ + {from: "1.26.0", want: "1.26.2"}, // 1.26.1 is broken too, skip it + {from: "1.26.1", want: "1.26.2"}, + {from: "1.21.0", want: "1.21.2"}, // 1.21.1 is broken too + {from: "1.24.0", want: "1.24.1"}, + {from: "1.27.0", want: "1.27.1"}, + {from: "1.30.0", want: "1.30.1"}, + } { + t.Run(tt.from, func(t *testing.T) { + got := version.MustNewVersionFromString(tt.from).NextNonBrokenPatch() + assert.Equal(t, tt.want, got.String()) + assert.False(t, version.IsBrokenRelease(got), "suggested version must not itself be broken") + }) + } +} + +func TestIsBrokenRelease(t *testing.T) { + assert.True(t, version.IsBrokenRelease(version.MustNewVersionFromString("1.30.0"))) + assert.False(t, version.IsBrokenRelease(version.MustNewVersionFromString("1.30.1"))) +} diff --git a/tests/e2e/assert_test.go b/tests/e2e/assert_test.go index 56c31cc6..4d32c759 100644 --- a/tests/e2e/assert_test.go +++ b/tests/e2e/assert_test.go @@ -19,6 +19,7 @@ package e2e import ( "context" + "fmt" "testing" "time" @@ -77,45 +78,80 @@ func AssertTemporalClusterCanBeUpgraded(v string) features.Func { } } +const ( + // The cluster reporting Ready does not guarantee the frontend Service has + // stopped routing to a pod that is still terminating from the rolling + // update, so connecting and running a workflow is retried rather than + // failed on the first error. + // + // This became load-bearing when TestPersistence stopped skipping five of its + // six cases: the suite went from 8 upgrade steps to 35, and each step makes + // this assertion. A per-step failure rate small enough to go unnoticed at 8 + // steps turns into a regularly red build at 35. + workflowAssertAttempts = 6 + workflowAssertInterval = 10 * time.Second +) + func AssertClusterCanHandleWorkflows() features.Func { return func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { cluster := GetTemporalClusterForFeature(ctx) - connectAddr, closePortForward, err := forwardPortToTemporalFrontend(ctx, cfg, t, cluster) - if err != nil { - t.Fatal(err) + + var err error + for attempt := 1; attempt <= workflowAssertAttempts; attempt++ { + err = runGreetingWorkflow(ctx, cfg, t, cluster) + if err == nil { + return ctx + } + + t.Logf("attempt %d/%d could not run a workflow: %v", attempt, workflowAssertAttempts, err) + if attempt < workflowAssertAttempts { + time.Sleep(workflowAssertInterval) + } } - defer closePortForward() - t.Logf("Temporal frontend addr: %s", connectAddr) + t.Fatalf("cluster could not handle workflows after %d attempts: %v", workflowAssertAttempts, err) - client := cfg.Client().Resources().GetControllerRuntimeClient() + return ctx + } +} - clusterClient, err := temporal.GetClusterClient(ctx, client, cluster, temporal.WithHostPort(connectAddr)) - if err != nil { - t.Fatal(err) - } +// runGreetingWorkflow performs one full connect-worker-workflow cycle against +// the cluster's frontend, returning an error instead of failing the test so the +// caller can retry. +func runGreetingWorkflow(ctx context.Context, cfg *envconf.Config, t *testing.T, cluster *v1beta1.TemporalCluster) error { + connectAddr, closePortForward, err := forwardPortToTemporalFrontend(ctx, cfg, t, cluster) + if err != nil { + return fmt.Errorf("can't forward port to frontend: %w", err) + } + defer closePortForward() - w, err := testworker.NewWorker(clusterClient) - if err != nil { - t.Fatal(err) - } + t.Logf("Temporal frontend addr: %s", connectAddr) - t.Log("Starting test worker") - err = w.Start() - if err != nil { - t.Fatal(err) - } + client := cfg.Client().Resources().GetControllerRuntimeClient() - defer w.Stop() + clusterClient, err := temporal.GetClusterClient(ctx, client, cluster, temporal.WithHostPort(connectAddr)) + if err != nil { + return fmt.Errorf("can't create cluster client: %w", err) + } - t.Logf("Starting workflow") - err = teststarter.NewStarter(clusterClient).StartGreetingWorkflow() - if err != nil { - t.Fatal(err) - } + w, err := testworker.NewWorker(clusterClient) + if err != nil { + return fmt.Errorf("can't create test worker: %w", err) + } - return ctx + t.Log("Starting test worker") + if err := w.Start(); err != nil { + return fmt.Errorf("can't start test worker: %w", err) } + + defer w.Stop() + + t.Logf("Starting workflow") + if err := teststarter.NewStarter(clusterClient).StartGreetingWorkflow(); err != nil { + return fmt.Errorf("can't start greeting workflow: %w", err) + } + + return nil } func AssertTemporalClusterWithMTLSCanHandleWorkflows() features.Func { diff --git a/tests/e2e/mtls_test.go b/tests/e2e/mtls_test.go index 6edec089..9cedb20a 100644 --- a/tests/e2e/mtls_test.go +++ b/tests/e2e/mtls_test.go @@ -147,7 +147,7 @@ func TestWithmTLSEnabled(t *testing.T) { }, } - featureTable := []features.Feature{} + featureTable := make([]features.Feature, 0, len(tests)) for name, testCase := range tests { test := testCase diff --git a/tests/e2e/persistence_test.go b/tests/e2e/persistence_test.go index 1e145ebe..bbc134e9 100644 --- a/tests/e2e/persistence_test.go +++ b/tests/e2e/persistence_test.go @@ -33,7 +33,31 @@ var ( initialClusterVersion = "1.19.1" newDatastoreVersion = "1.24.3" oldPersistenceUpgradePath = []string{"1.20.4", "1.21.2", "1.22.6", "1.23.0"} - defaultUpgradePath = []string{"1.25.2", "1.26.2", "1.27.2", "1.28.1"} + defaultUpgradePath = []string{"1.25.2", "1.26.2", "1.27.2", "1.28.1", "1.29.7", "1.30.5", "1.31.1"} + + // mysql8UpgradePath stops at 1.28.1. Upgrading a mysql8 cluster to 1.29.7 + // leaves it permanently un-Ready: the operator updates every Deployment + // successfully and then the pods never reach Ready, so the readiness wait + // times out after 600s. It reproduced on all four Kubernetes versions, and + // independently of where the case fell in the run order (in two of the four + // jobs mysql8 ran second, with the node barely loaded), so it is neither + // flaky nor resource exhaustion. + // + // Three plausible causes were ruled out directly: + // - the schema migration: running the real 1.17 -> 1.18 mysql8 update + // (v1.18/tasks_v2.sql) with temporal-sql-tool from admin-tools:1.29 + // against MySQL 8.4.11 succeeds cleanly; + // - the server itself: temporalio/auto-setup:1.29.7 with DB=mysql8 starts + // and serves against that same MySQL; + // - the schema content: mysql8 and postgresql12 get the same 1.17 -> 1.18 + // migration, and postgres12 walks the whole path fine. + // + // Root-causing it needs the failing pods' logs, which the e2e artifacts do + // not capture (kind exports logs after the namespace is torn down). Rather + // than block the SQL coverage this file exists to provide, mysql8 is held at + // the last version it is known to reach. Restore defaultUpgradePath here + // once the 1.29 failure is understood. + mysql8UpgradePath = []string{"1.25.2", "1.26.2", "1.27.2", "1.28.1"} ) type ( @@ -140,7 +164,22 @@ func TestPersistence(t *testing.T) { } }, }, - "postgres persistence with ES advanced visibility": { + // This case was dead *and* invalid before the skip filter above was + // removed. It declared spec.persistence.advancedVisibilityStore, which + // the webhook has forbidden for clusters >= 1.24 since 84722d5 + // (2024-09-26) -- "advanced visibility" became plain visibility in + // Temporal 1.24 -- while creating the cluster at 1.24.3. Admission would + // have rejected it. The skip filter arrived later, in 624c28f + // (2024-12-01), and hid that. + // + // It is revived here in the supported shape: Elasticsearch as the + // visibility store. It also runs at defaultVersion (>= 1.30) rather than + // 1.24.3, because that is where the ES code actually needs coverage: + // admin-tools >= 1.30 dropped curl/jq and the operator drives ES through + // temporal-elasticsearch-tool instead, which had no e2e coverage at all. + // Nothing is lost by not exercising the older curl path here, since this + // case has not run since 2024. + "postgres persistence with ES visibility": { upgradePath: []string{}, deployDependencies: []deployDependencyFunc{deployAndWaitForPostgres, deployAndWaitForElasticSearch}, cluster: func(_ context.Context, _ *envconf.Config, namespace string) *v1beta1.TemporalCluster { @@ -154,7 +193,7 @@ func TestPersistence(t *testing.T) { Spec: v1beta1.TemporalClusterSpec{ NumHistoryShards: 1, JobTTLSecondsAfterFinished: &jobTTL, - Version: version.MustNewVersionFromString(newDatastoreVersion), + Version: defaultVersion, Persistence: v1beta1.TemporalPersistenceSpec{ DefaultStore: &v1beta1.DatastoreSpec{ SQL: &v1beta1.SQLSpec{ @@ -170,19 +209,6 @@ func TestPersistence(t *testing.T) { }, }, VisibilityStore: &v1beta1.DatastoreSpec{ - SQL: &v1beta1.SQLSpec{ - User: "temporal", - PluginName: "postgres12", - DatabaseName: "temporal_visibility", - ConnectAddr: connectAddr, - ConnectProtocol: "tcp", - }, - PasswordSecretRef: &v1beta1.SecretKeyReference{ - Name: "postgres-password", - Key: "PASSWORD", - }, - }, - AdvancedVisibilityStore: &v1beta1.DatastoreSpec{ Elasticsearch: &v1beta1.ElasticsearchSpec{ Version: "v8", URL: "http://elasticsearch-es-http:9200", @@ -246,7 +272,8 @@ func TestPersistence(t *testing.T) { }, }, "mysql8 persistence": { - upgradePath: defaultUpgradePath, + // Held at 1.28.1; see mysql8UpgradePath for why. + upgradePath: mysql8UpgradePath, deployDependencies: []deployDependencyFunc{deployAndWaitForMySQL}, cluster: func(_ context.Context, _ *envconf.Config, namespace string) *v1beta1.TemporalCluster { connectAddr := fmt.Sprintf("mysql.%s:3306", namespace) @@ -340,12 +367,18 @@ func TestPersistence(t *testing.T) { }, } - featureTable := []features.Feature{} + featureTable := make([]features.Feature, 0, len(tests)) + // Every case runs. A filter that skipped all but "cassandra persistence" + // lived here from 624c28f (2024-12-01) until it was removed, which meant the + // pure-SQL upgrade paths -- the ones most deployments actually use -- were + // never exercised. The cassandra case does use postgres12 for its + // *visibility* store, so SQL visibility migrations had incidental coverage, + // but no case verified a SQL *default* store surviving an upgrade. + // + // This is the bulk of the suite's runtime: 35 version-steps across the six + // cases, against 8 when only cassandra ran. See E2E_TIMEOUT in the Makefile. for name, testCase := range tests { - if name != "cassandra persistence" { - continue - } test := testCase feature := features.New(name). Setup(func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { diff --git a/tests/e2e/utils_test.go b/tests/e2e/utils_test.go index f5269d8f..dae6516c 100644 --- a/tests/e2e/utils_test.go +++ b/tests/e2e/utils_test.go @@ -48,7 +48,7 @@ import ( const doesNotExistName = "does-not-exist" -var defaultVersion = version.MustNewVersionFromString("1.24.3") +var defaultVersion = version.MustNewVersionFromString("1.31.1") func deployAndWaitForTemporalWithPostgres(ctx context.Context, cfg *envconf.Config, namespace string) (*v1beta1.TemporalCluster, error) { // create the postgres @@ -57,10 +57,9 @@ func deployAndWaitForTemporalWithPostgres(ctx context.Context, cfg *envconf.Conf return nil, err } + // defaultVersion is well above 1.24, and both arms of the branch that used + // to be here assigned the same plugin anyway. pluginName := "postgres12" - if defaultVersion.GreaterOrEqual(version.V1_24_0) { - pluginName = "postgres12" - } connectAddr := fmt.Sprintf("postgres.%s:5432", namespace) // create the temporal cluster cluster := &v1beta1.TemporalCluster{ diff --git a/webhooks/temporalcluster_webhook.go b/webhooks/temporalcluster_webhook.go index ca02399b..3c4e3b60 100644 --- a/webhooks/temporalcluster_webhook.go +++ b/webhooks/temporalcluster_webhook.go @@ -30,7 +30,6 @@ import ( enumsspb "go.temporal.io/server/api/enums/v1" "go.temporal.io/server/common/primitives" apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/utils/ptr" "k8s.io/utils/strings/slices" @@ -44,14 +43,6 @@ type TemporalClusterWebhook struct { AvailableAPIs *discovery.AvailableAPIs } -func (w *TemporalClusterWebhook) getClusterFromRequest(obj runtime.Object) (*v1beta1.TemporalCluster, error) { - cluster, ok := obj.(*v1beta1.TemporalCluster) - if !ok { - return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an TemporalCluster but got a %T", obj)) - } - return cluster, nil -} - func (w *TemporalClusterWebhook) aggregateClusterErrors(cluster *v1beta1.TemporalCluster, errs field.ErrorList) error { if len(errs) == 0 { return nil @@ -65,12 +56,7 @@ func (w *TemporalClusterWebhook) aggregateClusterErrors(cluster *v1beta1.Tempora } // Default ensures empty fields have their default value. -func (w *TemporalClusterWebhook) Default(_ context.Context, obj runtime.Object) error { - cluster, err := w.getClusterFromRequest(obj) - if err != nil { - return err - } - +func (w *TemporalClusterWebhook) Default(_ context.Context, cluster *v1beta1.TemporalCluster) error { if cluster.Spec.Metrics.IsEnabled() { if cluster.Spec.Metrics.Prometheus != nil { // If the user has set the deprecated ListenAddress field and not the new ListenPort, @@ -210,15 +196,15 @@ func (w *TemporalClusterWebhook) validateCluster(cluster *v1beta1.TemporalCluste } // Check that the user-specified version is not marked as broken. - for _, version := range version.ForbiddenBrokenReleases { - if cluster.Spec.Version.Equal(version.Version) { - errs = append(errs, - field.Forbidden( - field.NewPath("spec", "version"), - fmt.Sprintf("version %s is marked as broken by the operator, please upgrade to %s (if allowed)", cluster.Spec.Version.String(), cluster.Spec.Version.IncPatch().String()), - ), - ) - } + // The suggested version skips any release that is itself broken, so users + // are not sent from one rejected version straight to another. + if version.IsBrokenRelease(cluster.Spec.Version) { + errs = append(errs, + field.Forbidden( + field.NewPath("spec", "version"), + fmt.Sprintf("version %s is marked as broken by the operator, please upgrade to %s (if allowed)", cluster.Spec.Version.String(), cluster.Spec.Version.NextNonBrokenPatch().String()), + ), + ) } // Check new features introduced in cluster version >= 1.20 are not enabled for older version. @@ -300,6 +286,44 @@ func (w *TemporalClusterWebhook) validateCluster(cluster *v1beta1.TemporalCluste } } + // Validate SQL passwordCommand usage. It resolves the datastore password by + // running an external command and is only supported by Temporal >= 1.31. + for name, store := range cluster.Spec.Persistence.GetDatastoresMap() { + if store == nil || store.SQL == nil || store.SQL.PasswordCommand == nil { + continue + } + path := field.NewPath("spec", "persistence", name, "sql", "passwordCommand") + if !cluster.Spec.Version.GreaterOrEqual(version.V1_31_0) { + errs = append(errs, field.Forbidden( + path, + "sql.passwordCommand requires Temporal >= 1.31.0.", + )) + } + if store.PasswordSecretRef != nil { + errs = append(errs, field.Forbidden( + path, + "sql.passwordCommand is mutually exclusive with passwordSecretRef.", + )) + } + if store.SQL.PasswordCommand.Command == "" { + errs = append(errs, field.Required( + path.Child("command"), + "command is required when passwordCommand is set.", + )) + } + // The persistence schema jobs run the same command inside the + // admin-tools image, and that image cannot be extended: the job's pod + // spec is fully operator-owned, so there is no volume or container + // override through which a helper binary could be supplied. If the + // command is not already present in admin-tools, schema setup fails + // with an authentication error even though the server pods themselves + // resolve the password fine. + warns = append(warns, fmt.Sprintf( + "%s: the command must already exist in the admin-tools image; the persistence schema jobs run it there and their pod spec cannot be extended with extra volumes or containers", + path.String(), + )) + } + // Check for per unit histogram boundaries if metrics is enabled if cluster.Spec.Metrics.IsEnabled() && cluster.Spec.Metrics.PerUnitHistogramBoundaries != nil { p := cluster.Spec.Metrics.PerUnitHistogramBoundaries @@ -362,12 +386,7 @@ func (w *TemporalClusterWebhook) validateCluster(cluster *v1beta1.TemporalCluste } // ValidateCreate ensures the user is creating a consistent temporal cluster. -func (w *TemporalClusterWebhook) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { - cluster, err := w.getClusterFromRequest(obj) - if err != nil { - return nil, err - } - +func (w *TemporalClusterWebhook) ValidateCreate(_ context.Context, cluster *v1beta1.TemporalCluster) (admission.Warnings, error) { warns, errs := w.validateCluster(cluster) return warns, w.aggregateClusterErrors(cluster, errs) @@ -375,17 +394,7 @@ func (w *TemporalClusterWebhook) ValidateCreate(_ context.Context, obj runtime.O // ValidateUpdate validates TemporalCluster updates. // It mainly check for sequential version upgrades. -func (w *TemporalClusterWebhook) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - oldCluster, err := w.getClusterFromRequest(oldObj) - if err != nil { - return nil, err - } - - newCluster, err := w.getClusterFromRequest(newObj) - if err != nil { - return nil, err - } - +func (w *TemporalClusterWebhook) ValidateUpdate(_ context.Context, oldCluster, newCluster *v1beta1.TemporalCluster) (admission.Warnings, error) { warns, errs := w.validateCluster(newCluster) // Ensure user is doing a sequential version upgrade. @@ -420,14 +429,13 @@ func (w *TemporalClusterWebhook) ValidateUpdate(_ context.Context, oldObj, newOb } // ValidateDelete does nothing. -func (w *TemporalClusterWebhook) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) { +func (w *TemporalClusterWebhook) ValidateDelete(_ context.Context, _ *v1beta1.TemporalCluster) (admission.Warnings, error) { // No delete validation needed. return nil, nil } func (w *TemporalClusterWebhook) SetupWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(&v1beta1.TemporalCluster{}). + return ctrl.NewWebhookManagedBy(mgr, &v1beta1.TemporalCluster{}). WithDefaulter(w). WithValidator(w). Complete() diff --git a/webhooks/temporalcluster_webhook_nulls_test.go b/webhooks/temporalcluster_webhook_nulls_test.go new file mode 100644 index 00000000..0c5ad2ac --- /dev/null +++ b/webhooks/temporalcluster_webhook_nulls_test.go @@ -0,0 +1,120 @@ +// Licensed to Alexandre VILAIN under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Alexandre VILAIN licenses this file to you 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 webhooks_test + +import ( + "context" + "encoding/json" + "fmt" + "sort" + "testing" + + "github.com/alexandrevilain/temporal-operator/api/v1beta1" + "github.com/alexandrevilain/temporal-operator/internal/discovery" + "github.com/alexandrevilain/temporal-operator/webhooks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// collectNullJSONPaths recursively walks an unmarshaled JSON document and +// collects the paths of all null values found. +func collectNullJSONPaths(path string, value interface{}, nulls *[]string) { + switch typedValue := value.(type) { + case nil: + *nulls = append(*nulls, path) + case map[string]interface{}: + for key, child := range typedValue { + collectNullJSONPaths(fmt.Sprintf("%s.%s", path, key), child, nulls) + } + case []interface{}: + for i, child := range typedValue { + collectNullJSONPaths(fmt.Sprintf("%s[%d]", path, i), child, nulls) + } + } +} + +// TestDefaultDoesNotSerializeNullValues ensures that the object returned by the +// defaulting webhook never serializes JSON null values. The api-server validates +// the mutating webhook's response against the CRD structural schema, which +// rejects null for non-nullable fields. Nil pointers, slices and maps whose +// json tag lacks omitempty would be serialized as null and make the api-server +// reject an otherwise valid TemporalCluster. +func TestDefaultDoesNotSerializeNullValues(t *testing.T) { + wh := &webhooks.TemporalClusterWebhook{ + AvailableAPIs: &discovery.AvailableAPIs{}, + } + + // A minimal valid TemporalCluster as a user would write it: + // authorization is set with authorizer and claimMapper but jwtKeyProvider + // is omitted, and every optional pointer field is left nil. + cluster := &v1beta1.TemporalCluster{ + TypeMeta: v1beta1.TemporalClusterTypeMeta, + ObjectMeta: metav1.ObjectMeta{ + Name: "fake", + Namespace: "default", + }, + Spec: v1beta1.TemporalClusterSpec{ + NumHistoryShards: 1, + Persistence: v1beta1.TemporalPersistenceSpec{ + DefaultStore: &v1beta1.DatastoreSpec{ + SQL: &v1beta1.SQLSpec{ + User: "temporal", + PluginName: "postgres", + DatabaseName: "temporal", + ConnectAddr: "postgres:5432", + }, + PasswordSecretRef: &v1beta1.SecretKeyReference{ + Name: "postgres-password", + }, + }, + VisibilityStore: &v1beta1.DatastoreSpec{ + SQL: &v1beta1.SQLSpec{ + User: "temporal", + PluginName: "postgres", + DatabaseName: "temporal_visibility", + ConnectAddr: "postgres:5432", + }, + PasswordSecretRef: &v1beta1.SecretKeyReference{ + Name: "postgres-password", + }, + }, + }, + Authorization: &v1beta1.AuthorizationSpec{ + Authorizer: "default", + ClaimMapper: "default", + }, + }, + } + + err := wh.Default(context.Background(), cluster) + require.NoError(t, err) + + data, err := json.Marshal(cluster.Spec) + require.NoError(t, err) + + var decoded interface{} + err = json.Unmarshal(data, &decoded) + require.NoError(t, err) + + nulls := []string{} + collectNullJSONPaths("spec", decoded, &nulls) + sort.Strings(nulls) + + assert.Empty(t, nulls, "defaulted TemporalCluster spec serializes JSON null values, which the CRD structural schema rejects") +} diff --git a/webhooks/temporalcluster_webhook_test.go b/webhooks/temporalcluster_webhook_test.go index cea97cc9..fc073656 100644 --- a/webhooks/temporalcluster_webhook_test.go +++ b/webhooks/temporalcluster_webhook_test.go @@ -19,6 +19,7 @@ package webhooks_test import ( "context" + "strings" "testing" "github.com/alexandrevilain/temporal-operator/api/v1beta1" @@ -26,16 +27,16 @@ import ( "github.com/alexandrevilain/temporal-operator/pkg/version" "github.com/alexandrevilain/temporal-operator/webhooks" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/utils/ptr" ) func TestDefault(t *testing.T) { tests := map[string]struct { - initialObject runtime.Object - expectedObject runtime.Object + initialObject *v1beta1.TemporalCluster + expectedObject *v1beta1.TemporalCluster expectedErr string }{ "default fields": { @@ -45,7 +46,7 @@ func TestDefault(t *testing.T) { Name: "fake", }, }, - expectedObject: func() runtime.Object { + expectedObject: func() *v1beta1.TemporalCluster { c := &v1beta1.TemporalCluster{ TypeMeta: v1beta1.TemporalClusterTypeMeta, ObjectMeta: metav1.ObjectMeta{ @@ -72,7 +73,7 @@ func TestDefault(t *testing.T) { }, }, }, - expectedObject: func() runtime.Object { + expectedObject: func() *v1beta1.TemporalCluster { c := &v1beta1.TemporalCluster{ TypeMeta: v1beta1.TemporalClusterTypeMeta, ObjectMeta: metav1.ObjectMeta{ @@ -145,7 +146,7 @@ func TestDefault(t *testing.T) { func TestValidateCreate(t *testing.T) { tests := map[string]struct { - object runtime.Object + object *v1beta1.TemporalCluster wh *webhooks.TemporalClusterWebhook expectedErr string }{ @@ -195,7 +196,9 @@ func TestValidateCreate(t *testing.T) { wh: &webhooks.TemporalClusterWebhook{ AvailableAPIs: &discovery.AvailableAPIs{}, }, - expectedErr: "TemporalCluster.temporal.io \"fake\" is invalid: spec.version: Forbidden: version 1.21.0 is marked as broken by the operator, please upgrade to 1.21.1 (if allowed)", + // 1.21.1 is broken too, so the suggestion has to skip it: telling a + // user to move to a version this same check rejects is a dead end. + expectedErr: "TemporalCluster.temporal.io \"fake\" is invalid: spec.version: Forbidden: version 1.21.0 is marked as broken by the operator, please upgrade to 1.21.2 (if allowed)", }, "error when no cert manager and mTLS with cert-manager enabled": { object: &v1beta1.TemporalCluster{ @@ -357,8 +360,8 @@ func TestValidateCreate(t *testing.T) { func TestValidateUpdate(t *testing.T) { tests := map[string]struct { - oldlObject runtime.Object - newObject runtime.Object + oldlObject *v1beta1.TemporalCluster + newObject *v1beta1.TemporalCluster expectedErr string }{ "allowed upgrade": { @@ -461,3 +464,58 @@ func TestValidateUpdate(t *testing.T) { }) } } + +// TestValidateCreate_PasswordCommandWarning asserts users are told about the +// one place sql.passwordCommand cannot work. +// +// The schema-setup jobs run the command inside the admin-tools image, and their +// pod spec is entirely operator-owned — there is no volume or container +// override through which a helper binary (an RDS/Cloud SQL IAM token generator, +// the documented use case) could be supplied. The server pods resolve the +// password natively, so the failure surfaces only as a password-authentication +// error from the very first schema job, which is hard to attribute. +func TestValidateCreate_PasswordCommandWarning(t *testing.T) { + wh := &webhooks.TemporalClusterWebhook{ + AvailableAPIs: &discovery.AvailableAPIs{}, + } + + cluster := &v1beta1.TemporalCluster{ + TypeMeta: v1beta1.TemporalClusterTypeMeta, + ObjectMeta: metav1.ObjectMeta{Name: "fake"}, + Spec: v1beta1.TemporalClusterSpec{ + Version: version.MustNewVersionFromString("1.31.1"), + Persistence: v1beta1.TemporalPersistenceSpec{ + DefaultStore: &v1beta1.DatastoreSpec{ + Name: "default", + SQL: &v1beta1.SQLSpec{ + User: "temporal", + PluginName: "postgres12", + DatabaseName: "temporal", + ConnectAddr: "postgres:5432", + PasswordCommand: &v1beta1.SQLPasswordCommandSpec{ + Command: "/usr/local/bin/rds-token", + Args: []string{"--host", "db"}, + }, + }, + }, + VisibilityStore: &v1beta1.DatastoreSpec{ + Name: "visibility", + SQL: &v1beta1.SQLSpec{ + User: "temporal", + PluginName: "postgres12", + DatabaseName: "temporal_visibility", + ConnectAddr: "postgres:5432", + }, + PasswordSecretRef: &v1beta1.SecretKeyReference{Name: "pg", Key: "PASSWORD"}, + }, + }, + }, + } + cluster.Default() + + warns, err := wh.ValidateCreate(context.Background(), cluster) + require.NoError(t, err) + require.NotEmpty(t, warns, "passwordCommand must warn about the admin-tools image") + assert.Contains(t, strings.Join(warns, "\n"), "admin-tools image") + assert.Contains(t, strings.Join(warns, "\n"), "spec.persistence.defaultStore.sql.passwordCommand") +}