From 206f6fc2c14f157facad5687856f0a61a13171f8 Mon Sep 17 00:00:00 2001 From: Alexander Laye Date: Fri, 5 Sep 2025 16:39:59 -0400 Subject: [PATCH 01/12] add plugin --- plugins/wal-replica/.gitignore | 7 + plugins/wal-replica/Dockerfile | 20 ++ plugins/wal-replica/LICENSE | 23 ++ plugins/wal-replica/README.md | 57 ++++ plugins/wal-replica/cmd/plugin/doc.go | 5 + plugins/wal-replica/cmd/plugin/plugin.go | 39 +++ plugins/wal-replica/doc/development.md | 180 ++++++++++++ .../cluster-example-no-parameters.yaml | 12 + .../cluster-example-with-mistake.yaml | 23 ++ .../doc/examples/cluster-example.yaml | 16 ++ .../example/cluster-with-wal-receiver.yaml | 61 ++++ plugins/wal-replica/go.mod | 94 +++++++ plugins/wal-replica/go.sum | 261 ++++++++++++++++++ plugins/wal-replica/internal/config/config.go | 105 +++++++ plugins/wal-replica/internal/config/doc.go | 5 + plugins/wal-replica/internal/identity/doc.go | 6 + plugins/wal-replica/internal/identity/impl.go | 50 ++++ plugins/wal-replica/internal/k8sclient/doc.go | 5 + .../internal/k8sclient/k8sclient.go | 63 +++++ plugins/wal-replica/internal/operator/doc.go | 6 + plugins/wal-replica/internal/operator/impl.go | 51 ++++ .../internal/operator/mutations.go | 56 ++++ .../wal-replica/internal/operator/status.go | 74 +++++ .../internal/operator/validation.go | 72 +++++ .../wal-replica/internal/reconciler/impl.go | 54 ++++ .../internal/reconciler/replica.go | 165 +++++++++++ plugins/wal-replica/internal/utils/doc.go | 5 + plugins/wal-replica/internal/utils/utils.go | 19 ++ .../kubernetes/certificate-issuer.yaml | 6 + .../kubernetes/client-certificate.yaml | 19 ++ .../wal-replica/kubernetes/deployment.yaml | 43 +++ .../wal-replica/kubernetes/kustomization.yaml | 13 + .../kubernetes/server-certificate.yaml | 21 ++ plugins/wal-replica/kubernetes/service.yaml | 18 ++ plugins/wal-replica/main.go | 33 +++ plugins/wal-replica/pkg/metadata/doc.go | 22 ++ .../wal-replica/release-please-config.json | 12 + plugins/wal-replica/renovate.json5 | 86 ++++++ plugins/wal-replica/scripts/build.sh | 6 + plugins/wal-replica/scripts/run.sh | 39 +++ 40 files changed, 1852 insertions(+) create mode 100644 plugins/wal-replica/.gitignore create mode 100644 plugins/wal-replica/Dockerfile create mode 100644 plugins/wal-replica/LICENSE create mode 100644 plugins/wal-replica/README.md create mode 100644 plugins/wal-replica/cmd/plugin/doc.go create mode 100644 plugins/wal-replica/cmd/plugin/plugin.go create mode 100644 plugins/wal-replica/doc/development.md create mode 100644 plugins/wal-replica/doc/examples/cluster-example-no-parameters.yaml create mode 100644 plugins/wal-replica/doc/examples/cluster-example-with-mistake.yaml create mode 100644 plugins/wal-replica/doc/examples/cluster-example.yaml create mode 100644 plugins/wal-replica/example/cluster-with-wal-receiver.yaml create mode 100644 plugins/wal-replica/go.mod create mode 100644 plugins/wal-replica/go.sum create mode 100644 plugins/wal-replica/internal/config/config.go create mode 100644 plugins/wal-replica/internal/config/doc.go create mode 100644 plugins/wal-replica/internal/identity/doc.go create mode 100644 plugins/wal-replica/internal/identity/impl.go create mode 100644 plugins/wal-replica/internal/k8sclient/doc.go create mode 100644 plugins/wal-replica/internal/k8sclient/k8sclient.go create mode 100644 plugins/wal-replica/internal/operator/doc.go create mode 100644 plugins/wal-replica/internal/operator/impl.go create mode 100644 plugins/wal-replica/internal/operator/mutations.go create mode 100644 plugins/wal-replica/internal/operator/status.go create mode 100644 plugins/wal-replica/internal/operator/validation.go create mode 100644 plugins/wal-replica/internal/reconciler/impl.go create mode 100644 plugins/wal-replica/internal/reconciler/replica.go create mode 100644 plugins/wal-replica/internal/utils/doc.go create mode 100644 plugins/wal-replica/internal/utils/utils.go create mode 100644 plugins/wal-replica/kubernetes/certificate-issuer.yaml create mode 100644 plugins/wal-replica/kubernetes/client-certificate.yaml create mode 100644 plugins/wal-replica/kubernetes/deployment.yaml create mode 100644 plugins/wal-replica/kubernetes/kustomization.yaml create mode 100644 plugins/wal-replica/kubernetes/server-certificate.yaml create mode 100644 plugins/wal-replica/kubernetes/service.yaml create mode 100644 plugins/wal-replica/main.go create mode 100644 plugins/wal-replica/pkg/metadata/doc.go create mode 100644 plugins/wal-replica/release-please-config.json create mode 100644 plugins/wal-replica/renovate.json5 create mode 100755 plugins/wal-replica/scripts/build.sh create mode 100755 plugins/wal-replica/scripts/run.sh diff --git a/plugins/wal-replica/.gitignore b/plugins/wal-replica/.gitignore new file mode 100644 index 00000000..57436766 --- /dev/null +++ b/plugins/wal-replica/.gitignore @@ -0,0 +1,7 @@ +bin/ +dist/ +.env +.vscode/ +.idea/ +.task/ +manifest.yaml diff --git a/plugins/wal-replica/Dockerfile b/plugins/wal-replica/Dockerfile new file mode 100644 index 00000000..69a5fddf --- /dev/null +++ b/plugins/wal-replica/Dockerfile @@ -0,0 +1,20 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Step 1: build image +FROM golang:1.24 AS builder + +# Cache the dependencies +WORKDIR /app +COPY go.mod go.sum /app/ +RUN go mod download + +# Compile the application +COPY . /app +RUN --mount=type=cache,target=/root/.cache/go-build ./scripts/build.sh + +# Step 2: build the image to be actually run +FROM golang:1-alpine +USER 10001:10001 +COPY --from=builder /app/bin/cnpg-i-wal-replica /app/bin/cnpg-i-wal-replica +ENTRYPOINT ["/app/bin/cnpg-i-wal-replica"] diff --git a/plugins/wal-replica/LICENSE b/plugins/wal-replica/LICENSE new file mode 100644 index 00000000..a5ed2a9a --- /dev/null +++ b/plugins/wal-replica/LICENSE @@ -0,0 +1,23 @@ +DocumentDB Kubernetes Operator + +Copyright (c) Microsoft Corporation. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/plugins/wal-replica/README.md b/plugins/wal-replica/README.md new file mode 100644 index 00000000..ee6d70a5 --- /dev/null +++ b/plugins/wal-replica/README.md @@ -0,0 +1,57 @@ +# WAL Receiver Pod Manager (CNPG-I Plugin) + +This plugin adds an optional standalone WAL receiver (pg_receivewal) Pod/Deployment +alongside a [CloudNativePG](https://github.com/cloudnative-pg/cloudnative-pg/) Cluster. +It reconciles a Deployment named +`-wal-receiver` that continuously streams WAL files from the primary +cluster using `pg_receivewal`, supporting synchronous mode. + +## Parameters + +Add the plugin in the Cluster spec (example): + +```yaml +spec: + plugins: + - name: cnpg-i-wal-replica.documentdb.io + parameters: + enabled: "true" + image: "ghcr.io/cloudnative-pg/postgresql:16" + replicationUser: streaming_replica + replicationPasswordSecretName: cluster-replication + replicationPasswordSecretKey: password # optional (default: password) + synchronous: "true" # optional (default true) + walDirectory: /var/lib/wal # optional (default /var/lib/wal) + # replicationHost: override-host.example # optional +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| enabled | bool | false | Enable or disable the plugin | +| image | string | ghcr.io/cloudnative-pg/postgresql:16 | Image providing pg_receivewal | +| replicationHost | string | -rw | Host to connect for streaming | +| replicationUser | string | streaming_replica | Replication user | +| replicationPasswordSecretName | string | (required when enabled) | Secret containing replication password | +| replicationPasswordSecretKey | string | password | Key in the secret | +| synchronous | bool | true | Add --synchronous flag to pg_receivewal | +| walDirectory | string | /var/lib/wal | Local directory to store WAL | + +The Deployment exposes a metrics port (9187) and creates a Service with the same name. + +## Build + +```bash +go build -o bin/cnpg-i-wal-replica main.go +``` + +## Status + +The plugin status reflects only whether it is enabled. + +## Future Work + +* Add PVC / volume configuration for WAL directory +* Expose resource requests/limits and security context +* Garbage collection / retention policy for archived WAL +* Liveness/readiness refinements + diff --git a/plugins/wal-replica/cmd/plugin/doc.go b/plugins/wal-replica/cmd/plugin/doc.go new file mode 100644 index 00000000..b7790c20 --- /dev/null +++ b/plugins/wal-replica/cmd/plugin/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Package plugin implements the command to start the plugin +package plugin diff --git a/plugins/wal-replica/cmd/plugin/plugin.go b/plugins/wal-replica/cmd/plugin/plugin.go new file mode 100644 index 00000000..2dc02548 --- /dev/null +++ b/plugins/wal-replica/cmd/plugin/plugin.go @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package plugin + +import ( + "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/http" + "github.com/cloudnative-pg/cnpg-i/pkg/operator" + "github.com/cloudnative-pg/cnpg-i/pkg/reconciler" + "github.com/spf13/cobra" + "google.golang.org/grpc" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + "github.com/documentdb/cnpg-i-wal-replica/internal/identity" + operatorImpl "github.com/documentdb/cnpg-i-wal-replica/internal/operator" + reconcilerImpl "github.com/documentdb/cnpg-i-wal-replica/internal/reconciler" +) + +// NewCmd creates the `plugin` command +func NewCmd() *cobra.Command { + cmd := http.CreateMainCmd(identity.Implementation{}, func(server *grpc.Server) error { + // Register the declared implementations + operator.RegisterOperatorServer(server, operatorImpl.Implementation{}) + reconciler.RegisterReconcilerHooksServer(server, reconcilerImpl.Implementation{}) + return nil + }) + + // If you want to provide your own logr.Logger here, inject it into a context.Context + // with logr.NewContext(ctx, logger) and pass it to cmd.SetContext(ctx) + logger := zap.New(zap.UseDevMode(true)) + log.SetLogger(logger) + + // Additional custom behaviour can be added by wrapping cmd.PersistentPreRun or cmd.Run + + cmd.Use = "plugin" + + return cmd +} diff --git a/plugins/wal-replica/doc/development.md b/plugins/wal-replica/doc/development.md new file mode 100644 index 00000000..b98e9cd1 --- /dev/null +++ b/plugins/wal-replica/doc/development.md @@ -0,0 +1,180 @@ +# Plugin Development + +This section of the documentation illustrates the CNPG-I capabilities used by +the wal-replica plugin, how the plugin implementation uses them, and how +developers can build and deploy the plugin. + +## Concepts + +### Identity + +The Identity interface defines the features supported by the plugin and is the +only interface that must always be implemented. + +This information is essential for the operator to discover the plugin's +capabilities during startup. + +The Identity interface provides: + +- A mechanism for plugins to report readiness probes. Readiness is a + prerequisite for receiving events, and plugins are expected to always report + the most accurate readiness data available. +- The capabilities reported by the plugin, which determine the subsequent calls + the plugin will receive. +- Metadata about the plugin. + +[API reference](https://github.com/cloudnative-pg/cnpg-i/blob/main/proto/identity.proto) + +### Capabilities + +This plugin implements the Operator and the Lifecycle capabilities. + +#### Operator + +This feature enables the plugin to receive events about the cluster creation and +mutations, this is defined by the following + +``` proto +// ValidateCreate improves the behavior of the validating webhook that +// is called on creation of the Cluster resources +rpc ValidateClusterCreate(OperatorValidateClusterCreateRequest) returns (OperatorValidateClusterCreateResult) {} + +// ValidateClusterChange improves the behavior of the validating webhook of +// is called on updates of the Cluster resources +rpc ValidateClusterChange(OperatorValidateClusterChangeRequest) returns (OperatorValidateClusterChangeResult) {} + +// MutateCluster fills in the defaults inside a Cluster resource +rpc MutateCluster(OperatorMutateClusterRequest) returns (OperatorMutateClusterResult) {} +``` + +This interface allows plugins to implement important features like: + +1. validating the cluster manifest during the creation and mutations + (it is expected that the plugin validate the parameters assigned to their + configuration). + +2. mutating the cluster object before it is submitted to kubernetes API server, + for example to set default values for the plugin parameters. + +[API reference](https://github.com/cloudnative-pg/cnpg-i/blob/main/proto/operator.proto) + +The wal-replica plugin is using this to validate used-defined parameters, and to +set default values for the labels and annotations applied by the plugin if not +specified by the user. + +#### Lifecycle + +This feature enables the plugin to receive events and create patches for +Kubernetes resources `before` they are submitted to the API server. + +To use this feature, the plugin must specify the resource and operation it wants +to be notified of. + +Some examples of what it can be achieved through the lifecycle: + +- add volume, volume mounts, sidecar containers, labels, annotations to pods, + especially necessary when implementing custom backup solutions +- modify any resource with some annotations or labels +- add/remove finalizers + +[API reference](https://github.com/cloudnative-pg/cnpg-i/blob/main/proto/operator_lifecycle.proto): + +The wal-replica plugin is using this to add labels, annotations and a sidecar +to the pods. + +## Implementation + +### Identity + +1. Define a struct inside the `internal/identity` package that implements + the `pluginhelper.IdentityServer` interface. + +2. Implement the following methods: + + - `GetPluginMetadata`: return human-readable information about the plugin. + - `GetPluginCapabilities`: specify the features supported by the plugin. In + the wal-replica example, the + `PluginCapability_Service_TYPE_LIFECYCLE_SERVICE` is defined in the + corresponding Go [file](../internal/lifecycle/lifecycle.go). + - `Probe`: indicate whether the plugin is ready to serve requests; this + example is stateless, so it will always be ready. + +### Lifecycle + +This example implements the lifecycle service capabilities to add labels and +annotations to the pods. The `OperatorLifecycleServer` interface is implemented +inside the `internal/lifecycle` package. + +The `OperatorLifecycleServer` interface requires several methods: + +- `GetCapabilities`: describe the resources and operations the plugin should be + notified for + +- `LifecycleHook`: is invoked for every operation against the Kubernetes API + server that matches the specifications returned by `GetCapabilities` + + In this function, the plugin is expected to do pattern matching using + the `Kind` and the operation `Type` and proceed with the proper logic. + +### Operator + +The operator interface offers a way for the plugin to interact with the Cluster +resource webhooks. + +Do that, the plugin should implement +the [operator](https://github.com/cloudnative-pg/cnpg-i/blob/main/proto/operator.proto) +interface, specifically the `MutateCluster`, `ValidateClusterCreate`, +and `ValidateClusterChange` rpc calls. + +- `MutateCluster`: enriches the plugin defaulting webhook + +- `ValidateClusterCreate` and `ValidateClusterChange`: enriches the plugin + validation logic. + +The package `internal/operator` implements this interface. + +### Startup Command + +The plugin runs in its own pod, and its main command is implemented in +the `main.go` file. + +This function uses the plugin helper library to create a GRPC server and manage +TLS. + +Plugin developers are expected to use the `pluginhelper.CreateMainCmd` +to implement the `main` function, passing an implemented `Identity` +struct. + +Further implementations can be registered within the callback function. + +In the example we propose, that's done for **operator** and for the +**lifecycle** services in [file](../cmd/plugin/plugin.go): + +``` proto +operator.RegisterOperatorServer(server, operatorImpl.Implementation{}) +lifecycle.RegisterOperatorLifecycleServer(server, lifecycleImpl.Implementation{}) +``` + +## Build and deploy the plugin + +Users can test their own changes to the plugin by building a container image +running it inside a Kubernetes cluster with CloudNativePG and cert-manager +installed. + +### Local build + +The repository provides a [`Taskfile`](https://taskfile.dev/) that contains +several helpful commands to test the plugin in +a [CNPG development environment](https://github.com/cloudnative-pg/cloudnative-pg/tree/main/contribute/e2e_testing_environment#the-local-kubernetes-cluster-for-testing). + +By executing `task local-kind-deploy`, a container image containing the +executable of the repository will be built and loaded inside the kind cluster. + +Having done that, the wal-replica plugin deployment will be applied. + +### CI/CD build + +The repository provides a GitHub Actions workflow that, on pushes, builds a +container image and generates a manifest file that can be used to deploy the +plugin. The manifest is attached to the workflow run as an artifact, and can be +applied to the cluster. diff --git a/plugins/wal-replica/doc/examples/cluster-example-no-parameters.yaml b/plugins/wal-replica/doc/examples/cluster-example-no-parameters.yaml new file mode 100644 index 00000000..a926fe66 --- /dev/null +++ b/plugins/wal-replica/doc/examples/cluster-example-no-parameters.yaml @@ -0,0 +1,12 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: cluster-example +spec: + instances: 3 + + plugins: + - name: cnpg-i-wal-replica.documentdb.io + + storage: + size: 1Gi diff --git a/plugins/wal-replica/doc/examples/cluster-example-with-mistake.yaml b/plugins/wal-replica/doc/examples/cluster-example-with-mistake.yaml new file mode 100644 index 00000000..0e2fe81e --- /dev/null +++ b/plugins/wal-replica/doc/examples/cluster-example-with-mistake.yaml @@ -0,0 +1,23 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: cluster-example +spec: + instances: 3 + + plugins: + - name: cnpg-i-wal-replica.documentdb.io + parameters: + labels: | + { + "first-label": "first-label-value", + "second-label": "second-label-value" + } + annotations: | + { + "first-annotation": "first-annotation-value", + this is a mistake + } + + storage: + size: 1Gi diff --git a/plugins/wal-replica/doc/examples/cluster-example.yaml b/plugins/wal-replica/doc/examples/cluster-example.yaml new file mode 100644 index 00000000..f592de4b --- /dev/null +++ b/plugins/wal-replica/doc/examples/cluster-example.yaml @@ -0,0 +1,16 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: cluster-example +spec: + instances: 1 + + plugins: + - name: cnpg-i-wal-replica.documentdb.io + parameters: + replicationHost: cluster-example-rw + + storage: + size: 1Gi + + logLevel: "debug" diff --git a/plugins/wal-replica/example/cluster-with-wal-receiver.yaml b/plugins/wal-replica/example/cluster-with-wal-receiver.yaml new file mode 100644 index 00000000..2ea35698 --- /dev/null +++ b/plugins/wal-replica/example/cluster-with-wal-receiver.yaml @@ -0,0 +1,61 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: sample-wal-cluster + namespace: default +spec: + instances: 1 + imageName: ghcr.io/cloudnative-pg/postgresql:16 + storage: + size: 1Gi + superuserSecret: + name: sample-wal-superuser + bootstrap: + initdb: + database: appdb + owner: appuser + secret: + name: sample-wal-app-user + postgresql: + parameters: + wal_level: replica + archive_mode: "on" + archive_timeout: "60s" + plugins: + - name: cnpg-i-wal-replica.documentdb.io + parameters: + enabled: "true" + replicationUser: streaming_replica + replicationPasswordSecretName: sample-wal-repl + synchronous: "true" + walDirectory: /var/lib/wal + # optional service exposure + primaryUpdateStrategy: unsupervised + enableSuperuserAccess: true +--- +apiVersion: v1 +kind: Secret +metadata: + name: sample-wal-superuser + namespace: default +stringData: + username: postgres + password: supersecret +--- +apiVersion: v1 +kind: Secret +metadata: + name: sample-wal-app-user + namespace: default +stringData: + username: appuser + password: appsecret +--- +apiVersion: v1 +kind: Secret +metadata: + name: sample-wal-repl + namespace: default +stringData: + username: streaming_replica + password: replsecret diff --git a/plugins/wal-replica/go.mod b/plugins/wal-replica/go.mod new file mode 100644 index 00000000..03058ec4 --- /dev/null +++ b/plugins/wal-replica/go.mod @@ -0,0 +1,94 @@ +module github.com/documentdb/cnpg-i-wal-replica + +go 1.23.5 + +toolchain go1.24.0 + +require ( + github.com/cloudnative-pg/api v1.25.1 + github.com/cloudnative-pg/cnpg-i v0.1.0 + github.com/cloudnative-pg/cnpg-i-machinery v0.2.0 + github.com/cloudnative-pg/machinery v0.1.0 + github.com/spf13/cobra v1.9.1 + google.golang.org/grpc v1.71.0 + k8s.io/api v0.32.3 + k8s.io/apimachinery v0.32.3 + k8s.io/utils v0.0.0-20241210054802-24370beab758 + sigs.k8s.io/controller-runtime v0.20.3 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudnative-pg/barman-cloud v0.1.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.12.1 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mitchellh/mapstructure v1.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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.80.1 // indirect + github.com/prometheus/client_golang v1.21.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/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/snorwin/jsonpatch v1.5.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/spf13/viper v1.19.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/net v0.36.0 // indirect + golang.org/x/oauth2 v0.27.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/term v0.29.0 // indirect + golang.org/x/text v0.22.0 // indirect + golang.org/x/time v0.9.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.32.2 // indirect + k8s.io/client-go v0.32.2 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/plugins/wal-replica/go.sum b/plugins/wal-replica/go.sum new file mode 100644 index 00000000..903e773a --- /dev/null +++ b/plugins/wal-replica/go.sum @@ -0,0 +1,261 @@ +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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudnative-pg/api v1.25.1 h1:uNjKiB0MIspUeH9l651SnFDcuflr1crB3t6LjxUCafQ= +github.com/cloudnative-pg/api v1.25.1/go.mod h1:fwF5g4XkuNZqYXIeRR3AJvUfWlqWig+r2DXc5bEmw6U= +github.com/cloudnative-pg/barman-cloud v0.1.0 h1:e/z52CehMBIh1LjZqNBJnncWJbS+1JYvRMBR8Js6Uiw= +github.com/cloudnative-pg/barman-cloud v0.1.0/go.mod h1:rJUJO/f1yNckLZiVxHAyRmKY+4EPJkYRJsGbTZRJQSY= +github.com/cloudnative-pg/cnpg-i v0.1.0 h1:QH2xTsrODMhEEc6B25GbOYe7ZIttDmSkYvXotfU5dfs= +github.com/cloudnative-pg/cnpg-i v0.1.0/go.mod h1:G28BhgUEHqrxEyyQeHz8BbpMVAsGuLhJm/tHUbDi8Sw= +github.com/cloudnative-pg/cnpg-i-machinery v0.2.0 h1:htNuKirdAOYrc7Hu5mLDoOES+nKSyPaXNDLgbV5dLSI= +github.com/cloudnative-pg/cnpg-i-machinery v0.2.0/go.mod h1:MHVxMMbLeCRnEM8PLWW4C2CsHqOeAU2OsrwWMKy3tPA= +github.com/cloudnative-pg/machinery v0.1.0 h1:tjRmsqQmsO/OlaT0uFmkEtVqgr+SGPM88cKZOHYKLBo= +github.com/cloudnative-pg/machinery v0.1.0/go.mod h1:0V3vm44FaIsY+x4pm8ORry7xCC3AJiO+ebfPNxeP5Ck= +github.com/cpuguy83/go-md2man/v2 v2.0.6/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/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/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +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.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-faker/faker/v4 v4.4.1 h1:LY1jDgjVkBZWIhATCt+gkl0x9i/7wC61gZx73GTFb+Q= +github.com/go-faker/faker/v4 v4.4.1/go.mod h1:HRLrjis+tYsbFtIHufEPTAIzcZiRu0rS9EYl2Ccwme4= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +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.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 h1:kQ0NI7W1B3HwiN5gAYtY+XFItDPbLBwYRxAqbFTyDes= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0/go.mod h1:zrT2dxOAjNFPRGjTUe2Xmb4q4YdUwVvQFV6xiCSf+z0= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +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/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.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +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/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +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/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/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= +github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.80.1 h1:DP+PUNVOc+Bkft8a4QunLzaZ0RspWuD3tBbcPHr2PeE= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.80.1/go.mod h1:6x4x0t9BP35g4XcjkHE9EB3RxhyfxpdpmZKd/Qyk8+M= +github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= +github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +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/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/snorwin/jsonpatch v1.5.0 h1:0m56YSt9cHiJOn8U+OcqdPGcDQZmhPM/zsG7Dv5QQP0= +github.com/snorwin/jsonpatch v1.5.0/go.mod h1:e0IDKlyFBLTFPqM0wa79dnMwjMs3XFvmKcrgCRpDqok= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/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/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +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.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +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/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +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/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.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= +golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +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.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +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.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-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.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= +golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= +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/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +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= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= +k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= +k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= +k8s.io/apiextensions-apiserver v0.32.2 h1:2YMk285jWMk2188V2AERy5yDwBYrjgWYggscghPCvV4= +k8s.io/apiextensions-apiserver v0.32.2/go.mod h1:GPwf8sph7YlJT3H6aKUWtd0E+oyShk/YHWQHf/OOgCA= +k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= +k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA= +k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94= +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-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg= +k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7/go.mod h1:GewRfANuJ70iYzvn+i4lezLDAFzvjxZYK1gn1lWcfas= +k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= +k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.20.3 h1:I6Ln8JfQjHH7JbtCD2HCYHoIzajoRxPNuvhvcDbZgkI= +sigs.k8s.io/controller-runtime v0.20.3/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +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/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk= +sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/plugins/wal-replica/internal/config/config.go b/plugins/wal-replica/internal/config/config.go new file mode 100644 index 00000000..950e2b98 --- /dev/null +++ b/plugins/wal-replica/internal/config/config.go @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package config + +import ( + "fmt" + "strings" + + "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/common" + "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/validation" + "github.com/cloudnative-pg/cnpg-i/pkg/operator" +) + +// Plugin parameter keys +const ( + ImageParam = "image" // string + ReplicationHostParam = "replicationHost" // Required: primary host + SynchronousParam = "synchronous" // enum: Active, Inactive, Unset + WalDirectoryParam = "walDirectory" // directory where WAL is stored +) + +// SynchronousMode represents the synchronous replication mode +type SynchronousMode string + +const ( + SynchronousUnset SynchronousMode = "" + SynchronousActive SynchronousMode = "active" + SynchronousInactive SynchronousMode = "inactive" +) + +const ( + defaultImage = "ghcr.io/cloudnative-pg/postgresql:16" + defaultWalDir = "/var/lib/postgres/wal" + defaultSynchronousMode = SynchronousInactive +) + +// Configuration represents the plugin configuration parameters controlling the wal receiver pod +type Configuration struct { + Image string + ReplicationHost string + Synchronous SynchronousMode + WalDirectory string +} + +// FromParameters builds a plugin configuration from the configuration parameters +func FromParameters(helper *common.Plugin) *Configuration { + cfg := &Configuration{} + cfg.Image = helper.Parameters[ImageParam] + cfg.ReplicationHost = helper.Parameters[ReplicationHostParam] + cfg.Synchronous = SynchronousMode(strings.ToLower(helper.Parameters[SynchronousParam])) + cfg.WalDirectory = helper.Parameters[WalDirectoryParam] + return cfg +} + +// ValidateChanges validates the changes between the old configuration to the new configuration +func ValidateChanges(_ *Configuration, _ *Configuration, _ *common.Plugin) []*operator.ValidationError { + return nil +} + +// ToParameters serialize the configuration back to plugin parameters +func (c *Configuration) ToParameters() (map[string]string, error) { + params := map[string]string{} + params[ImageParam] = c.Image + params[ReplicationHostParam] = c.ReplicationHost + params[SynchronousParam] = string(c.Synchronous) + params[WalDirectoryParam] = c.WalDirectory + return params, nil +} + +// ValidateParams ensures that the provided parameters are valid +func ValidateParams(helper *common.Plugin) []*operator.ValidationError { + validationErrors := make([]*operator.ValidationError, 0) + + // Must be present + if raw, present := helper.Parameters[ReplicationHostParam]; !present || raw == "" { + validationErrors = append(validationErrors, validation.BuildErrorForParameter(helper, ReplicationHostParam, "No replication host provided")) + } + + // If present, must be valid + if raw, present := helper.Parameters[SynchronousParam]; present && raw != "" { + switch SynchronousMode(strings.ToLower(raw)) { + case SynchronousActive, SynchronousInactive: + // valid value + default: + validationErrors = append(validationErrors, validation.BuildErrorForParameter(helper, SynchronousParam, + fmt.Sprintf("Invalid value '%s'. Must be 'active' or 'inactive'", raw))) + } + } + return validationErrors +} + +// applyDefaults fills the configuration with the defaults +// We know that replicationhost and sync are valid already +func (c *Configuration) ApplyDefaults() { + if c.Image == "" { + c.Image = defaultImage + } + if c.WalDirectory == "" { + c.WalDirectory = defaultWalDir + } + if c.Synchronous == SynchronousUnset { + c.Synchronous = defaultSynchronousMode + } +} diff --git a/plugins/wal-replica/internal/config/doc.go b/plugins/wal-replica/internal/config/doc.go new file mode 100644 index 00000000..fd2e2564 --- /dev/null +++ b/plugins/wal-replica/internal/config/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Package config represents the plugin configuration +package config diff --git a/plugins/wal-replica/internal/identity/doc.go b/plugins/wal-replica/internal/identity/doc.go new file mode 100644 index 00000000..ec39c588 --- /dev/null +++ b/plugins/wal-replica/internal/identity/doc.go @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Package identity contains the implementation of the +// identity service +package identity diff --git a/plugins/wal-replica/internal/identity/impl.go b/plugins/wal-replica/internal/identity/impl.go new file mode 100644 index 00000000..05f63879 --- /dev/null +++ b/plugins/wal-replica/internal/identity/impl.go @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package identity + +import ( + "context" + + "github.com/cloudnative-pg/cnpg-i/pkg/identity" + + "github.com/documentdb/cnpg-i-wal-replica/pkg/metadata" +) + +// Implementation is the implementation of the identity service +type Implementation struct { + identity.IdentityServer +} + +// GetPluginMetadata implements the IdentityServer interface +func (Implementation) GetPluginMetadata( + context.Context, + *identity.GetPluginMetadataRequest, +) (*identity.GetPluginMetadataResponse, error) { + return &metadata.Data, nil +} + +// GetPluginCapabilities implements the IdentityServer interface +func (Implementation) GetPluginCapabilities( + context.Context, + *identity.GetPluginCapabilitiesRequest, +) (*identity.GetPluginCapabilitiesResponse, error) { + return &identity.GetPluginCapabilitiesResponse{ + Capabilities: []*identity.PluginCapability{ + { + Type: &identity.PluginCapability_Service_{ + Service: &identity.PluginCapability_Service{ + Type: identity.PluginCapability_Service_TYPE_RECONCILER_HOOKS, + }, + }, + }, + }, + }, nil +} + +// Probe implements the IdentityServer interface +func (Implementation) Probe(context.Context, *identity.ProbeRequest) (*identity.ProbeResponse, error) { + return &identity.ProbeResponse{ + Ready: true, + }, nil +} diff --git a/plugins/wal-replica/internal/k8sclient/doc.go b/plugins/wal-replica/internal/k8sclient/doc.go new file mode 100644 index 00000000..8cf08d78 --- /dev/null +++ b/plugins/wal-replica/internal/k8sclient/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Package k8sclient ensure a Kubernetes client is available +package k8sclient diff --git a/plugins/wal-replica/internal/k8sclient/k8sclient.go b/plugins/wal-replica/internal/k8sclient/k8sclient.go new file mode 100644 index 00000000..a6975c55 --- /dev/null +++ b/plugins/wal-replica/internal/k8sclient/k8sclient.go @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package k8sclient + +import ( + "sync" + + apiv1 "github.com/cloudnative-pg/api/pkg/api/v1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" +) + +var ( + currentClient client.Client + mu sync.Mutex + scheme *runtime.Scheme +) + +// MustGet gets a Kubernetes client or panics is it cannot find it +func MustGet() client.Client { + cl, err := Get() + if err != nil { + panic(err) + } + + return cl +} + +// Get gets the Kubernetes client, creating it if needed. If an error during the +// creation of the Kubernetes client is raised, it will be returned +func Get() (client.Client, error) { + if currentClient != nil { + return currentClient, nil + } + + mu.Lock() + defer mu.Unlock() + + currentConfig, err := config.GetConfig() + if err != nil { + return nil, err + } + + newClient, err := client.New(currentConfig, client.Options{Scheme: scheme}) + if err != nil { + return nil, err + } + + currentClient = newClient + + return currentClient, nil +} + +func init() { + scheme = runtime.NewScheme() + _ = apiv1.AddToScheme(scheme) + _ = appsv1.AddToScheme(scheme) + _ = corev1.AddToScheme(scheme) +} diff --git a/plugins/wal-replica/internal/operator/doc.go b/plugins/wal-replica/internal/operator/doc.go new file mode 100644 index 00000000..15752638 --- /dev/null +++ b/plugins/wal-replica/internal/operator/doc.go @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Package operator contains the implementation of the +// operator service +package operator diff --git a/plugins/wal-replica/internal/operator/impl.go b/plugins/wal-replica/internal/operator/impl.go new file mode 100644 index 00000000..285fdef5 --- /dev/null +++ b/plugins/wal-replica/internal/operator/impl.go @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package operator + +import ( + "context" + + "github.com/cloudnative-pg/cnpg-i/pkg/operator" +) + +// Implementation is the implementation of the identity service +type Implementation struct { + operator.OperatorServer +} + +// GetCapabilities gets the capabilities of this operator lifecycle hook +func (Implementation) GetCapabilities( + context.Context, + *operator.OperatorCapabilitiesRequest, +) (*operator.OperatorCapabilitiesResult, error) { + return &operator.OperatorCapabilitiesResult{ + Capabilities: []*operator.OperatorCapability{ + { + Type: &operator.OperatorCapability_Rpc{ + Rpc: &operator.OperatorCapability_RPC{ + Type: operator.OperatorCapability_RPC_TYPE_VALIDATE_CLUSTER_CREATE, + }, + }, + }, + { + Type: &operator.OperatorCapability_Rpc{ + Rpc: &operator.OperatorCapability_RPC{ + Type: operator.OperatorCapability_RPC_TYPE_VALIDATE_CLUSTER_CHANGE, + }, + }, + }, + { + Type: &operator.OperatorCapability_Rpc{ + Rpc: &operator.OperatorCapability_RPC{ + Type: operator.OperatorCapability_RPC_TYPE_SET_STATUS_IN_CLUSTER, + }, + }, + }, + }, + }, nil +} + +func (Implementation) Deregister(context.Context, *operator.DeregisterRequest) (*operator.DeregisterResponse, error) { + return &operator.DeregisterResponse{}, nil +} diff --git a/plugins/wal-replica/internal/operator/mutations.go b/plugins/wal-replica/internal/operator/mutations.go new file mode 100644 index 00000000..4d37b287 --- /dev/null +++ b/plugins/wal-replica/internal/operator/mutations.go @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package operator + +import ( + "context" + + "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/common" + "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/decoder" + "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/object" + "github.com/cloudnative-pg/cnpg-i/pkg/operator" + + "github.com/documentdb/cnpg-i-wal-replica/internal/config" + "github.com/documentdb/cnpg-i-wal-replica/pkg/metadata" +) + +// MutateCluster is called to mutate a cluster with the defaulting webhook. +// This function is defaulting the "imagePullPolicy" plugin parameter +func (Implementation) MutateCluster( + _ context.Context, + request *operator.OperatorMutateClusterRequest, +) (*operator.OperatorMutateClusterResult, error) { + cluster, err := decoder.DecodeClusterLenient(request.GetDefinition()) + if err != nil { + return nil, err + } + + helper := common.NewPlugin( + *cluster, + metadata.PluginName, + ) + + config := config.FromParameters(helper) + mutatedCluster := cluster.DeepCopy() + if helper.PluginIndex < 0 { + if mutatedCluster.Spec.Plugins[helper.PluginIndex].Parameters == nil { + mutatedCluster.Spec.Plugins[helper.PluginIndex].Parameters = make(map[string]string) + } + config.ApplyDefaults() + + mutatedCluster.Spec.Plugins[helper.PluginIndex].Parameters, err = config.ToParameters() + if err != nil { + return nil, err + } + } + + patch, err := object.CreatePatch(cluster, mutatedCluster) + if err != nil { + return nil, err + } + + return &operator.OperatorMutateClusterResult{ + JsonPatch: patch, + }, nil +} diff --git a/plugins/wal-replica/internal/operator/status.go b/plugins/wal-replica/internal/operator/status.go new file mode 100644 index 00000000..6f21279e --- /dev/null +++ b/plugins/wal-replica/internal/operator/status.go @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package operator + +import ( + "context" + "encoding/json" + "errors" + + apiv1 "github.com/cloudnative-pg/api/pkg/api/v1" + "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/clusterstatus" + "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/common" + "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/decoder" + "github.com/cloudnative-pg/cnpg-i/pkg/operator" + "github.com/cloudnative-pg/machinery/pkg/log" + + "github.com/documentdb/cnpg-i-wal-replica/pkg/metadata" +) + +type Status struct { + Enabled bool `json:"enabled"` +} + +func (Implementation) SetStatusInCluster( + ctx context.Context, + req *operator.SetStatusInClusterRequest, +) (*operator.SetStatusInClusterResponse, error) { + logger := log.FromContext(ctx).WithName("SetStatusInCluster") + + cluster, err := decoder.DecodeClusterLenient(req.GetCluster()) + if err != nil { + return nil, err + } + + // TODO remove + logger.Debug("Debug worked?") + + plg := common.NewPlugin(*cluster, metadata.PluginName) + + // Find the status for our plugin + var pluginEntry *apiv1.PluginStatus + for idx, entry := range plg.Cluster.Status.PluginStatus { + if metadata.PluginName == entry.Name { + pluginEntry = &plg.Cluster.Status.PluginStatus[idx] + break + } + } + + if pluginEntry == nil { + err := errors.New("plugin entry not found in the cluster status") + logger.Error(err, "while fetching the plugin status", "plugin", metadata.PluginName) + return nil, errors.New("plugin entry not found") + } + + var status Status + if pluginEntry.Status != "" { + if err := json.Unmarshal([]byte(pluginEntry.Status), &status); err != nil { + logger.Error(err, "while unmarshalling plugin status", + "entry", pluginEntry) + return nil, err + } + } + + if status.Enabled { + logger.Debug("plugin is enabled, no action taken") + return clusterstatus.NewSetStatusInClusterResponseBuilder().NoOpResponse(), nil + } + + // TODO uncomment this line when the `enabled` field stops alternating constantly + //logger.Info("setting enabled plugin status") + + return clusterstatus.NewSetStatusInClusterResponseBuilder().JSONStatusResponse(Status{Enabled: true}) +} diff --git a/plugins/wal-replica/internal/operator/validation.go b/plugins/wal-replica/internal/operator/validation.go new file mode 100644 index 00000000..65653c2f --- /dev/null +++ b/plugins/wal-replica/internal/operator/validation.go @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package operator + +import ( + "context" + + "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/common" + "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/decoder" + "github.com/cloudnative-pg/cnpg-i/pkg/operator" + + "github.com/documentdb/cnpg-i-wal-replica/internal/config" + "github.com/documentdb/cnpg-i-wal-replica/pkg/metadata" +) + +// ValidateClusterCreate validates a cluster that is being created, +// Should validate all plugin parameters +func (Implementation) ValidateClusterCreate( + _ context.Context, + request *operator.OperatorValidateClusterCreateRequest, +) (*operator.OperatorValidateClusterCreateResult, error) { + cluster, err := decoder.DecodeClusterLenient(request.GetDefinition()) + if err != nil { + return nil, err + } + + result := &operator.OperatorValidateClusterCreateResult{} + + helper := common.NewPlugin( + *cluster, + metadata.PluginName, + ) + + result.ValidationErrors = config.ValidateParams(helper) + + return result, nil +} + +// ValidateClusterChange validates a cluster that is being changed +func (Implementation) ValidateClusterChange( + _ context.Context, + request *operator.OperatorValidateClusterChangeRequest, +) (*operator.OperatorValidateClusterChangeResult, error) { + result := &operator.OperatorValidateClusterChangeResult{} + + oldCluster, err := decoder.DecodeClusterLenient(request.GetOldCluster()) + if err != nil { + return nil, err + } + + newCluster, err := decoder.DecodeClusterLenient(request.GetNewCluster()) + if err != nil { + return nil, err + } + + oldClusterHelper := common.NewPlugin( + *oldCluster, + metadata.PluginName, + ) + + newClusterHelper := common.NewPlugin( + *newCluster, + metadata.PluginName, + ) + + newConfiguration := config.FromParameters(newClusterHelper) + oldConfiguration := config.FromParameters(oldClusterHelper) + result.ValidationErrors = config.ValidateChanges(oldConfiguration, newConfiguration, newClusterHelper) + + return result, nil +} diff --git a/plugins/wal-replica/internal/reconciler/impl.go b/plugins/wal-replica/internal/reconciler/impl.go new file mode 100644 index 00000000..a35eec08 --- /dev/null +++ b/plugins/wal-replica/internal/reconciler/impl.go @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package reconciler + +import ( + "context" + "encoding/json" + + apiv1 "github.com/cloudnative-pg/api/pkg/api/v1" + "github.com/cloudnative-pg/cnpg-i/pkg/reconciler" + "github.com/cloudnative-pg/machinery/pkg/log" +) + +// Implementation is the implementation of the identity service +type Implementation struct { + reconciler.UnimplementedReconcilerHooksServer +} + +// GetCapabilities gets the capabilities of this operator lifecycle hook +func (Implementation) GetCapabilities( + context.Context, + *reconciler.ReconcilerHooksCapabilitiesRequest, +) (*reconciler.ReconcilerHooksCapabilitiesResult, error) { + return &reconciler.ReconcilerHooksCapabilitiesResult{ + ReconcilerCapabilities: []*reconciler.ReconcilerHooksCapability{ + { + Kind: reconciler.ReconcilerHooksCapability_KIND_CLUSTER, + }, + }, + }, nil +} + +func (Implementation) Post(ctx context.Context, req *reconciler.ReconcilerHooksRequest) (*reconciler.ReconcilerHooksResult, error) { + logger := log.FromContext(ctx).WithName("PostReconcilerHook") + cluster := &apiv1.Cluster{} + if err := json.Unmarshal(req.GetResourceDefinition(), cluster); err != nil { + logger.Error(err, "while decoding the cluster") + return nil, err + } + logger.Info("Post called for ", "cluster", cluster) + + if err := CreateWalReplica(ctx, cluster); err != nil { + logger.Error(err, "while creating the wal replica") + return nil, err + } + + return &reconciler.ReconcilerHooksResult{Behavior: reconciler.ReconcilerHooksResult_BEHAVIOR_CONTINUE}, nil +} + +func (Implementation) Pre(ctx context.Context, req *reconciler.ReconcilerHooksRequest) (*reconciler.ReconcilerHooksResult, error) { + // NOOP + return &reconciler.ReconcilerHooksResult{}, nil +} diff --git a/plugins/wal-replica/internal/reconciler/replica.go b/plugins/wal-replica/internal/reconciler/replica.go new file mode 100644 index 00000000..bc0e895a --- /dev/null +++ b/plugins/wal-replica/internal/reconciler/replica.go @@ -0,0 +1,165 @@ +package reconciler + +import ( + "context" + "fmt" + + apiv1 "github.com/cloudnative-pg/api/pkg/api/v1" + "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/common" + "github.com/cloudnative-pg/machinery/pkg/log" + "github.com/documentdb/cnpg-i-wal-replica/internal/config" + "github.com/documentdb/cnpg-i-wal-replica/internal/k8sclient" + "github.com/documentdb/cnpg-i-wal-replica/pkg/metadata" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func CreateWalReplica( + ctx context.Context, + cluster *apiv1.Cluster, +) error { + logger := log.FromContext(ctx).WithName("CreateWalReplica") + + if !IsPrimaryCluster(cluster) { + logger.Info("Cluster is not a primary, skipping wal replica creation", "cluster", cluster.Name) + return nil + } + + // Build Deployment name unique per cluster + deploymentName := fmt.Sprintf("%s-wal-receiver", cluster.Name) + namespace := cluster.Namespace + client := k8sclient.MustGet() + + helper := common.NewPlugin( + *cluster, + metadata.PluginName, + ) + + configuration := config.FromParameters(helper) + + walDir := configuration.WalDirectory + cmd := []string{ + "/usr/bin/postgresql/16/bin/pg_receivewal", // TODO find the actual path + "--slot", "wal_replica", + "--compress", "0", + "--directory", walDir, + "--host", configuration.ReplicationHost, + "--port", "5432", + "--username", "postgres", + "--no-password", + "--verbose", + } + + // Add synchronous flag if requested + if configuration.Synchronous == config.SynchronousActive { + cmd = append(cmd, "--synchronous") + } + + // Create a pVC + // Needs a PVC to store the wal data + existingPVC := &corev1.PersistentVolumeClaim{} + err := client.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: namespace}, existingPVC) + if err != nil && errors.IsNotFound(err) { + log.Info("WAL replica PVC not found. Creating a new WAL replica PVC") + + walReplicaPVC := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: deploymentName, + Namespace: namespace, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteOnce, + }, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("10Gi"), + }, + }, + }, + } + + err = client.Create(ctx, walReplicaPVC) + if err != nil { + return err + } + } else if err != nil { + return err + } + + // Create or patch Deployment + existing := &appsv1.Deployment{} + err = client.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: namespace}, existing) + if err != nil { + dep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: deploymentName, + Namespace: namespace, + Labels: map[string]string{ + "app": deploymentName, + "cnpg.io/cluster": cluster.Name, + }, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": deploymentName}}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": deploymentName}}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "wal-receiver", + Image: configuration.Image, + Args: cmd, + VolumeMounts: []corev1.VolumeMount{ + { + Name: deploymentName, + MountPath: walDir, + }, + }, + }}, + Volumes: []corev1.Volume{ + { + Name: deploymentName, + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: deploymentName, + }, + }, + }, + }, + SecurityContext: &corev1.PodSecurityContext{ + RunAsUser: int64Ptr(105), + RunAsGroup: int64Ptr(103), + FSGroup: int64Ptr(103), + }, + RestartPolicy: corev1.RestartPolicyAlways, + }, + }, + }, + } + // optional service for metrics + svc := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: deploymentName, Namespace: namespace, Labels: map[string]string{"app": deploymentName}}, Spec: corev1.ServiceSpec{Selector: map[string]string{"app": deploymentName}, Ports: []corev1.ServicePort{{Name: "metrics", Port: 9187, TargetPort: intstr.FromInt(9187)}}}} + if createErr := client.Create(ctx, dep); createErr != nil { + logger.Error(createErr, "creating wal receiver deployment") + return createErr + } + _ = client.Create(ctx, svc) // ignore error if exists + logger.Info("created wal receiver deployment", "name", deploymentName) + } else { + // TODO handle patch + } + + return nil +} +func int64Ptr(i int64) *int64 { + return &i +} + +func IsPrimaryCluster(cluster *apiv1.Cluster) bool { + // TODO implement + return true +} diff --git a/plugins/wal-replica/internal/utils/doc.go b/plugins/wal-replica/internal/utils/doc.go new file mode 100644 index 00000000..a9bff958 --- /dev/null +++ b/plugins/wal-replica/internal/utils/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Package utils contains methods to interact with kubernetes resources +package utils diff --git a/plugins/wal-replica/internal/utils/utils.go b/plugins/wal-replica/internal/utils/utils.go new file mode 100644 index 00000000..1867f3a2 --- /dev/null +++ b/plugins/wal-replica/internal/utils/utils.go @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package utils + +import "encoding/json" + +// GetKind gets the Kubernetes object kind from its JSON representation +func GetKind(definition []byte) (string, error) { + var genericObject struct { + Kind string `json:"kind"` + } + + if err := json.Unmarshal(definition, &genericObject); err != nil { + return "", err + } + + return genericObject.Kind, nil +} diff --git a/plugins/wal-replica/kubernetes/certificate-issuer.yaml b/plugins/wal-replica/kubernetes/certificate-issuer.yaml new file mode 100644 index 00000000..8d3d6eee --- /dev/null +++ b/plugins/wal-replica/kubernetes/certificate-issuer.yaml @@ -0,0 +1,6 @@ +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: selfsigned-issuer +spec: + selfSigned: {} diff --git a/plugins/wal-replica/kubernetes/client-certificate.yaml b/plugins/wal-replica/kubernetes/client-certificate.yaml new file mode 100644 index 00000000..58c79b9a --- /dev/null +++ b/plugins/wal-replica/kubernetes/client-certificate.yaml @@ -0,0 +1,19 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: walreplica-client +spec: + secretName: walreplica-client-tls + + commonName: "walreplica-client" + duration: 2160h # 90d + renewBefore: 360h # 15d + + isCA: false + usages: + - client auth + + issuerRef: + name: selfsigned-issuer + kind: Issuer + group: cert-manager.io diff --git a/plugins/wal-replica/kubernetes/deployment.yaml b/plugins/wal-replica/kubernetes/deployment.yaml new file mode 100644 index 00000000..a2314adc --- /dev/null +++ b/plugins/wal-replica/kubernetes/deployment.yaml @@ -0,0 +1,43 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: wal-replica + name: wal-replica +spec: + replicas: 1 + selector: + matchLabels: + app: wal-replica + strategy: {} + template: + metadata: + creationTimestamp: null + labels: + app: wal-replica + spec: + containers: + - image: cnpg-i-wal-replica:latest + name: cnpg-i-wal-replica + ports: + - containerPort: 9090 + protocol: TCP + args: + - plugin + - --server-cert=/server/tls.crt + - --server-key=/server/tls.key + - --client-cert=/client/tls.crt + - --server-address=:9090 + volumeMounts: + - mountPath: /server + name: server + - mountPath: /client + name: client + resources: {} + volumes: + - name: server + secret: + secretName: walreplica-server-tls + - name: client + secret: + secretName: walreplica-client-tls diff --git a/plugins/wal-replica/kubernetes/kustomization.yaml b/plugins/wal-replica/kubernetes/kustomization.yaml new file mode 100644 index 00000000..af21bb1a --- /dev/null +++ b/plugins/wal-replica/kubernetes/kustomization.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: cnpg-system +resources: +- certificate-issuer.yaml +- client-certificate.yaml +- deployment.yaml +- server-certificate.yaml +- service.yaml +images: +- name: cnpg-i-wal-replica + newName: ghcr.io/microsoft/documentdb-kubernetes-operator/wal-replica + newTag: latest diff --git a/plugins/wal-replica/kubernetes/server-certificate.yaml b/plugins/wal-replica/kubernetes/server-certificate.yaml new file mode 100644 index 00000000..1f573eab --- /dev/null +++ b/plugins/wal-replica/kubernetes/server-certificate.yaml @@ -0,0 +1,21 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: walreplica-server +spec: + secretName: walreplica-server-tls + commonName: "wal-replica" + dnsNames: + - wal-replica + + duration: 2160h # 90d + renewBefore: 360h # 15d + + isCA: false + usages: + - server auth + + issuerRef: + name: selfsigned-issuer + kind: Issuer + group: cert-manager.io diff --git a/plugins/wal-replica/kubernetes/service.yaml b/plugins/wal-replica/kubernetes/service.yaml new file mode 100644 index 00000000..b2217cb1 --- /dev/null +++ b/plugins/wal-replica/kubernetes/service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: wal-replica + cnpg.io/pluginName: cnpg-i-wal-replica.documentdb.io + annotations: + cnpg.io/pluginClientSecret: walreplica-client-tls + cnpg.io/pluginServerSecret: walreplica-server-tls + cnpg.io/pluginPort: "9090" + name: wal-replica +spec: + ports: + - port: 9090 + protocol: TCP + targetPort: 9090 + selector: + app: wal-replica diff --git a/plugins/wal-replica/main.go b/plugins/wal-replica/main.go new file mode 100644 index 00000000..0c7f251e --- /dev/null +++ b/plugins/wal-replica/main.go @@ -0,0 +1,33 @@ +// Package main is the entrypoint of the application +package main + +import ( + "fmt" + "os" + + "github.com/cloudnative-pg/machinery/pkg/log" + "github.com/spf13/cobra" + + "github.com/documentdb/cnpg-i-wal-replica/cmd/plugin" +) + +func main() { + logFlags := &log.Flags{} + rootCmd := &cobra.Command{ + Use: "cnpg-i-wal-replica", + Short: "WAL Replica", + PersistentPreRunE: func(_ *cobra.Command, _ []string) error { + logFlags.ConfigureLogging() + return nil + }, + } + + logFlags.AddFlags(rootCmd.PersistentFlags()) + + rootCmd.AddCommand(plugin.NewCmd()) + + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/plugins/wal-replica/pkg/metadata/doc.go b/plugins/wal-replica/pkg/metadata/doc.go new file mode 100644 index 00000000..cca11b21 --- /dev/null +++ b/plugins/wal-replica/pkg/metadata/doc.go @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Package metadata contains the metadata of this plugin +package metadata + +import "github.com/cloudnative-pg/cnpg-i/pkg/identity" + +// PluginName is the name of the plugin +const PluginName = "cnpg-i-wal-replica.documentdb.io" + +// Data is the metadata of this plugin +var Data = identity.GetPluginMetadataResponse{ + Name: PluginName, + Version: "0.1.0", + DisplayName: "WAL Replica Pod Manager", + ProjectUrl: "https://github.com/documentdb/cnpg-i-wal-replica", + RepositoryUrl: "https://github.com/documentdb/cnpg-i-wal-replica", + License: "Proprietary", + LicenseUrl: "https://github.com/documentdb/cnpg-i-wal-replica/LICENSE", + Maturity: "alpha", +} diff --git a/plugins/wal-replica/release-please-config.json b/plugins/wal-replica/release-please-config.json new file mode 100644 index 00000000..b3f9a1dc --- /dev/null +++ b/plugins/wal-replica/release-please-config.json @@ -0,0 +1,12 @@ +{ + "changelog-path": "CHANGELOG.md", + "release-type": "go", + "bump-minor-pre-major": false, + "bump-patch-for-minor-pre-major": false, + "draft": false, + "prerelease": false, + "packages": { + ".": {} + }, + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" +} diff --git a/plugins/wal-replica/renovate.json5 b/plugins/wal-replica/renovate.json5 new file mode 100644 index 00000000..fa60c475 --- /dev/null +++ b/plugins/wal-replica/renovate.json5 @@ -0,0 +1,86 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended", + ":gitSignOff", + ":semanticCommitType(chore)", + ":labels(automated,no-issue)", + "customManagers:githubActionsVersions", + ":automergeMinor", + ":automergeDigest" + ], + "prConcurrentLimit": 5, + "ignorePaths": [ + "doc/**", + "CHANGELOG.md" + ], + "postUpdateOptions": [ + "gomodTidy" + ], + "semanticCommits": "enabled", + "commitBodyTable": true, + // Allow renovate to update the following types of dependencies in the Taskfile.yml: + // - digests for env variables ending in _SHA + // - versions for env variables ending in _VERSION + "customManagers": [ + { + "customType": "regex", + "fileMatch": [ + "(^Taskfile\\.yml$)" + ], + "matchStrings": [ + "# renovate: datasource=(?[a-z-.]+?) depName=(?[^\\s]+?)(?: (?:lookupName|packageName)=(?[^\\s]+?))?(?: versioning=(?[^\\s]+?))?(?: extractVersion=(?[^\\s]+?))?(?: currentValue=(?[^\\s]+?))?\\s+[A-Za-z0-9_]+?_SHA\\s*:\\s*[\"']?(?[a-f0-9]+?)[\"']?\\s", + "# renovate: datasource=(?[a-z-.]+?) depName=(?[^\\s]+?)(?: (?:lookupName|packageName)=(?[^\\s]+?))?(?: versioning=(?[^\\s]+?))?(?: extractVersion=(?[^\\s]+?))?\\s+[A-Za-z0-9_]+?_VERSION\\s*:\\s*[\"']?(?.+?)[\"']?\\s" + ] + } + ], + "packageRules": [ + { + "matchDatasources": [ + "go" + ], + "matchPackageNames": [ + // Avoid k8s dependencies from being grouped with other dependencies. We want to be careful + // with how we update them. + "!/k8s.io/" + ], + "matchUpdateTypes": [ + "minor", + "patch", + "digest" + ], + "groupName": "all non-major go dependencies" + }, + { + "matchDatasources": [ + "git-refs" + ], + "matchPackageNames": [ + "https://github.com/cloudnative-pg/daggerverse" + ], + "matchUpdateTypes": [ + "digest" + ], + "groupName": "all cloudnative-pg daggerverse dependencies" + }, + { + "matchDatasources": [ + "git-refs" + ], + "matchPackageNames": [ + "https://github.com/sagikazarmark/daggerverse" + ], + "matchUpdateTypes": [ + "digest" + ], + "groupName": "all sagikazarmark daggerverse dependencies" + }, + { + "matchUpdateTypes": [ + "minor", + "patch" + ], + "matchCurrentVersion": "!/^0/" + } + ] +} diff --git a/plugins/wal-replica/scripts/build.sh b/plugins/wal-replica/scripts/build.sh new file mode 100755 index 00000000..df30b287 --- /dev/null +++ b/plugins/wal-replica/scripts/build.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +cd "$(dirname "$0")/.." || exit + +# Compile the plugin +CGO_ENABLED=0 go build -o bin/cnpg-i-wal-replica main.go diff --git a/plugins/wal-replica/scripts/run.sh b/plugins/wal-replica/scripts/run.sh new file mode 100755 index 00000000..e9c7da2b --- /dev/null +++ b/plugins/wal-replica/scripts/run.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +set -eu + +cd "$(dirname "$0")/.." || exit + +if [ -f .env ]; then + source .env +fi + +# The following script builds the plugin image and uploads it to the +# current kind cluster +# WARNING: This will fail with recent releases of kind due to https://github.com/kubernetes-sigs/kind/issues/3853 +# See fix in CNPG https://github.com/cloudnative-pg/cloudnative-pg/pull/6770 +# current_context=$(kubectl config view --raw -o json | jq -r '."current-context"' | sed "s/kind-//") +# kind load docker-image --name=${current_context} cnpg-i-wal-replica:${VERSION:-latest} + +# Constants +registry_name=registry.dev + +load_image_registry() { + local image=$1 + + local image_reg_name=${registry_name}:5000/${image} + local image_local_name=${image_reg_name/${registry_name}/127.0.0.1} + docker tag "${image}" "${image_reg_name}" + docker tag "${image}" "${image_local_name}" + docker push -q "${image_local_name}" +} + +# Now we deploy the plugin inside the `cnpg-system` workspace +kubectl apply -k kubernetes/ + +# We load the image into the registry (which is a prerequisite) +load_image_registry cnpg-i-wal-replica:${VERSION:-latest} + +# Patch the deployment to use the provided image +kubectl patch deployments.apps -n cnpg-system wal-replica -p \ + "{\"spec\":{\"template\":{\"spec\":{\"containers\":[{\"name\":\"cnpg-i-wal-replica\",\"image\":\"${registry_name}:5000/cnpg-i-wal-replica:${VERSION:-latest}\"}]}}}}" From ee447cba2d47d1b317b5b45ef94bde545fc08c2d Mon Sep 17 00:00:00 2001 From: Alexander Laye Date: Mon, 8 Sep 2025 11:45:52 -0400 Subject: [PATCH 02/12] use new service account --- plugins/wal-replica/internal/config/config.go | 10 +++++++ .../internal/reconciler/replica.go | 9 ++++++- .../wal-replica/kubernetes/deployment.yaml | 1 + .../wal-replica/kubernetes/kustomization.yaml | 3 +++ .../wal-replica/kubernetes/role-binding.yaml | 15 +++++++++++ plugins/wal-replica/kubernetes/role.yaml | 26 +++++++++++++++++++ .../kubernetes/service-account.yaml | 8 ++++++ 7 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 plugins/wal-replica/kubernetes/role-binding.yaml create mode 100644 plugins/wal-replica/kubernetes/role.yaml create mode 100644 plugins/wal-replica/kubernetes/service-account.yaml diff --git a/plugins/wal-replica/internal/config/config.go b/plugins/wal-replica/internal/config/config.go index 950e2b98..72df6f91 100644 --- a/plugins/wal-replica/internal/config/config.go +++ b/plugins/wal-replica/internal/config/config.go @@ -10,6 +10,7 @@ import ( "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/common" "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/validation" "github.com/cloudnative-pg/cnpg-i/pkg/operator" + "k8s.io/apimachinery/pkg/api/resource" ) // Plugin parameter keys @@ -18,6 +19,7 @@ const ( ReplicationHostParam = "replicationHost" // Required: primary host SynchronousParam = "synchronous" // enum: Active, Inactive, Unset WalDirectoryParam = "walDirectory" // directory where WAL is stored + WalPVCSize = "walPVCSize" // Size of the PVC for WAL storage ) // SynchronousMode represents the synchronous replication mode @@ -87,6 +89,14 @@ func ValidateParams(helper *common.Plugin) []*operator.ValidationError { fmt.Sprintf("Invalid value '%s'. Must be 'active' or 'inactive'", raw))) } } + + // If present, Wal size must be valid + if raw, present := helper.Parameters[WalPVCSize]; present && raw != "" { + if _, err := resource.ParseQuantity(raw); err != nil { + validationErrors = append(validationErrors, validation.BuildErrorForParameter(helper, WalPVCSize, err.Error())) + } + } + return validationErrors } diff --git a/plugins/wal-replica/internal/reconciler/replica.go b/plugins/wal-replica/internal/reconciler/replica.go index bc0e895a..6e289064 100644 --- a/plugins/wal-replica/internal/reconciler/replica.go +++ b/plugins/wal-replica/internal/reconciler/replica.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + package reconciler import ( @@ -52,7 +55,11 @@ func CreateWalReplica( "--port", "5432", "--username", "postgres", "--no-password", - "--verbose", + } + + // TODO have a real check here + if true { + cmd = append(cmd, "--verbose") } // Add synchronous flag if requested diff --git a/plugins/wal-replica/kubernetes/deployment.yaml b/plugins/wal-replica/kubernetes/deployment.yaml index a2314adc..1ae02479 100644 --- a/plugins/wal-replica/kubernetes/deployment.yaml +++ b/plugins/wal-replica/kubernetes/deployment.yaml @@ -16,6 +16,7 @@ spec: labels: app: wal-replica spec: + serviceAccountName: wal-replica-manager containers: - image: cnpg-i-wal-replica:latest name: cnpg-i-wal-replica diff --git a/plugins/wal-replica/kubernetes/kustomization.yaml b/plugins/wal-replica/kubernetes/kustomization.yaml index af21bb1a..b1f0287a 100644 --- a/plugins/wal-replica/kubernetes/kustomization.yaml +++ b/plugins/wal-replica/kubernetes/kustomization.yaml @@ -5,7 +5,10 @@ resources: - certificate-issuer.yaml - client-certificate.yaml - deployment.yaml +- role-binding.yaml +- role.yaml - server-certificate.yaml +- service-account.yaml - service.yaml images: - name: cnpg-i-wal-replica diff --git a/plugins/wal-replica/kubernetes/role-binding.yaml b/plugins/wal-replica/kubernetes/role-binding.yaml new file mode 100644 index 00000000..0542860d --- /dev/null +++ b/plugins/wal-replica/kubernetes/role-binding.yaml @@ -0,0 +1,15 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: wal-replia-manager + app.kubernetes.io/managed-by: kustomize + name: wal-replica-manager-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: wal-replica-manager +subjects: +- kind: ServiceAccount + name: wal-replica-manager + namespace: cnpg-system diff --git a/plugins/wal-replica/kubernetes/role.yaml b/plugins/wal-replica/kubernetes/role.yaml new file mode 100644 index 00000000..aa3d37a0 --- /dev/null +++ b/plugins/wal-replica/kubernetes/role.yaml @@ -0,0 +1,26 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: wal-replica-manager +rules: # TODO trim this down to what's actually needed +- apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] +- apiGroups: [""] + resources: ["services", "pods", "endpoints", "leases", "serviceaccounts", "configmaps", "namespaces"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] +- apiGroups: ["metrics.k8s.io"] + resources: ["pods"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] +- apiGroups: ["rbac.authorization.k8s.io"] + resources: ["roles", "rolebindings"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] +- apiGroups: ["coordination.k8s.io"] + resources: ["leases"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] +- apiGroups: [""] + resources: ["secrets", "persistentvolumeclaims"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] +- apiGroups: ["postgresql.cnpg.io"] + resources: ["clusters", "publications", "subscriptions"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] \ No newline at end of file diff --git a/plugins/wal-replica/kubernetes/service-account.yaml b/plugins/wal-replica/kubernetes/service-account.yaml new file mode 100644 index 00000000..cd9c6956 --- /dev/null +++ b/plugins/wal-replica/kubernetes/service-account.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: wal-replica-manager + namespace: cnpg-system + labels: + app.kubernetes.io/name: wal-replia-manager + app.kubernetes.io/managed-by: kustomize \ No newline at end of file From 28cae363d392a6c4fa51e5498d3fb717ae46a117 Mon Sep 17 00:00:00 2001 From: Alexander Laye Date: Mon, 8 Sep 2025 17:40:04 -0400 Subject: [PATCH 03/12] fix reconciler and disable operator --- plugins/wal-replica/README.md | 12 +- plugins/wal-replica/cmd/plugin/plugin.go | 6 +- .../cluster-example-with-mistake.yaml | 13 +- .../example/cluster-with-wal-receiver.yaml | 61 ------- plugins/wal-replica/go.mod | 129 ++++++++------- plugins/wal-replica/go.sum | 149 ++++++++++++++++++ plugins/wal-replica/internal/config/config.go | 6 + plugins/wal-replica/internal/identity/impl.go | 7 + plugins/wal-replica/internal/operator/impl.go | 13 +- .../internal/operator/mutations.go | 11 +- .../wal-replica/internal/operator/status.go | 19 ++- .../internal/operator/validation.go | 9 +- .../internal/reconciler/replica.go | 5 +- plugins/wal-replica/internal/utils/doc.go | 5 - plugins/wal-replica/internal/utils/utils.go | 19 --- .../wal-replica/kubernetes/deployment.yaml | 2 +- 16 files changed, 284 insertions(+), 182 deletions(-) delete mode 100644 plugins/wal-replica/example/cluster-with-wal-receiver.yaml delete mode 100644 plugins/wal-replica/internal/utils/doc.go delete mode 100644 plugins/wal-replica/internal/utils/utils.go diff --git a/plugins/wal-replica/README.md b/plugins/wal-replica/README.md index ee6d70a5..d9b11a57 100644 --- a/plugins/wal-replica/README.md +++ b/plugins/wal-replica/README.md @@ -15,24 +15,16 @@ spec: plugins: - name: cnpg-i-wal-replica.documentdb.io parameters: - enabled: "true" image: "ghcr.io/cloudnative-pg/postgresql:16" - replicationUser: streaming_replica - replicationPasswordSecretName: cluster-replication - replicationPasswordSecretKey: password # optional (default: password) - synchronous: "true" # optional (default true) + replicationHost: cluster-name-rw + synchronous: "enabled" # optional (default true) walDirectory: /var/lib/wal # optional (default /var/lib/wal) - # replicationHost: override-host.example # optional ``` | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| enabled | bool | false | Enable or disable the plugin | | image | string | ghcr.io/cloudnative-pg/postgresql:16 | Image providing pg_receivewal | | replicationHost | string | -rw | Host to connect for streaming | -| replicationUser | string | streaming_replica | Replication user | -| replicationPasswordSecretName | string | (required when enabled) | Secret containing replication password | -| replicationPasswordSecretKey | string | password | Key in the secret | | synchronous | bool | true | Add --synchronous flag to pg_receivewal | | walDirectory | string | /var/lib/wal | Local directory to store WAL | diff --git a/plugins/wal-replica/cmd/plugin/plugin.go b/plugins/wal-replica/cmd/plugin/plugin.go index 2dc02548..b0dc4f97 100644 --- a/plugins/wal-replica/cmd/plugin/plugin.go +++ b/plugins/wal-replica/cmd/plugin/plugin.go @@ -26,14 +26,10 @@ func NewCmd() *cobra.Command { return nil }) - // If you want to provide your own logr.Logger here, inject it into a context.Context - // with logr.NewContext(ctx, logger) and pass it to cmd.SetContext(ctx) logger := zap.New(zap.UseDevMode(true)) log.SetLogger(logger) - // Additional custom behaviour can be added by wrapping cmd.PersistentPreRun or cmd.Run - - cmd.Use = "plugin" + cmd.Use = "receivewal" return cmd } diff --git a/plugins/wal-replica/doc/examples/cluster-example-with-mistake.yaml b/plugins/wal-replica/doc/examples/cluster-example-with-mistake.yaml index 0e2fe81e..c46aa637 100644 --- a/plugins/wal-replica/doc/examples/cluster-example-with-mistake.yaml +++ b/plugins/wal-replica/doc/examples/cluster-example-with-mistake.yaml @@ -8,16 +8,7 @@ spec: plugins: - name: cnpg-i-wal-replica.documentdb.io parameters: - labels: | - { - "first-label": "first-label-value", - "second-label": "second-label-value" - } - annotations: | - { - "first-annotation": "first-annotation-value", - this is a mistake - } - + replication_host: "cluster-example-0.cluster-example-svc.cnpg-system.svc.cluster.local" + synchronous: "badvalue" storage: size: 1Gi diff --git a/plugins/wal-replica/example/cluster-with-wal-receiver.yaml b/plugins/wal-replica/example/cluster-with-wal-receiver.yaml deleted file mode 100644 index 2ea35698..00000000 --- a/plugins/wal-replica/example/cluster-with-wal-receiver.yaml +++ /dev/null @@ -1,61 +0,0 @@ -apiVersion: postgresql.cnpg.io/v1 -kind: Cluster -metadata: - name: sample-wal-cluster - namespace: default -spec: - instances: 1 - imageName: ghcr.io/cloudnative-pg/postgresql:16 - storage: - size: 1Gi - superuserSecret: - name: sample-wal-superuser - bootstrap: - initdb: - database: appdb - owner: appuser - secret: - name: sample-wal-app-user - postgresql: - parameters: - wal_level: replica - archive_mode: "on" - archive_timeout: "60s" - plugins: - - name: cnpg-i-wal-replica.documentdb.io - parameters: - enabled: "true" - replicationUser: streaming_replica - replicationPasswordSecretName: sample-wal-repl - synchronous: "true" - walDirectory: /var/lib/wal - # optional service exposure - primaryUpdateStrategy: unsupervised - enableSuperuserAccess: true ---- -apiVersion: v1 -kind: Secret -metadata: - name: sample-wal-superuser - namespace: default -stringData: - username: postgres - password: supersecret ---- -apiVersion: v1 -kind: Secret -metadata: - name: sample-wal-app-user - namespace: default -stringData: - username: appuser - password: appsecret ---- -apiVersion: v1 -kind: Secret -metadata: - name: sample-wal-repl - namespace: default -stringData: - username: streaming_replica - password: replsecret diff --git a/plugins/wal-replica/go.mod b/plugins/wal-replica/go.mod index 03058ec4..59d3d087 100644 --- a/plugins/wal-replica/go.mod +++ b/plugins/wal-replica/go.mod @@ -1,94 +1,109 @@ module github.com/documentdb/cnpg-i-wal-replica -go 1.23.5 - -toolchain go1.24.0 +go 1.24.1 require ( - github.com/cloudnative-pg/api v1.25.1 - github.com/cloudnative-pg/cnpg-i v0.1.0 - github.com/cloudnative-pg/cnpg-i-machinery v0.2.0 - github.com/cloudnative-pg/machinery v0.1.0 - github.com/spf13/cobra v1.9.1 - google.golang.org/grpc v1.71.0 - k8s.io/api v0.32.3 - k8s.io/apimachinery v0.32.3 - k8s.io/utils v0.0.0-20241210054802-24370beab758 - sigs.k8s.io/controller-runtime v0.20.3 + github.com/cloudnative-pg/api v1.27.0 + github.com/cloudnative-pg/cnpg-i v0.3.0 + github.com/cloudnative-pg/cnpg-i-machinery v0.4.0 + github.com/cloudnative-pg/machinery v0.3.1 + github.com/spf13/cobra v1.10.1 + google.golang.org/grpc v1.75.0 + k8s.io/api v0.34.0 + k8s.io/apimachinery v0.34.0 + sigs.k8s.io/controller-runtime v0.22.1 ) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudnative-pg/barman-cloud v0.1.0 // indirect + github.com/cloudnative-pg/barman-cloud v0.3.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.1 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/jsonpointer v0.22.0 // indirect + github.com/go-openapi/jsonreference v0.21.1 // indirect + github.com/go-openapi/swag v0.24.1 // indirect + github.com/go-openapi/swag/cmdutils v0.24.0 // indirect + github.com/go-openapi/swag/conv v0.24.0 // indirect + github.com/go-openapi/swag/fileutils v0.24.0 // indirect + github.com/go-openapi/swag/jsonname v0.24.0 // indirect + github.com/go-openapi/swag/jsonutils v0.24.0 // indirect + github.com/go-openapi/swag/loading v0.24.0 // indirect + github.com/go-openapi/swag/mangling v0.24.0 // indirect + github.com/go-openapi/swag/netutils v0.24.0 // indirect + github.com/go-openapi/swag/stringutils v0.24.0 // indirect + github.com/go-openapi/swag/typeutils v0.24.0 // indirect + github.com/go-openapi/swag/yamlutils v0.24.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/gnostic-models v0.6.9 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.11 // indirect - github.com/magiconair/properties v1.8.7 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/magiconair/properties v1.8.10 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/mitchellh/mapstructure v1.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/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.80.1 // indirect - github.com/prometheus/client_golang v1.21.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/sagikazarmark/locafero v0.4.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.85.0 // 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.17.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/snorwin/jsonpatch v1.5.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/pflag v1.0.6 // indirect - github.com/spf13/viper v1.19.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.21.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/net v0.36.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/term v0.29.0 // indirect - golang.org/x/text v0.22.0 // indirect - golang.org/x/time v0.9.0 // indirect - gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/oauth2 v0.31.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/term v0.35.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/time v0.13.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect + google.golang.org/protobuf v1.36.8 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.32.2 // indirect - k8s.io/client-go v0.32.2 // indirect + k8s.io/apiextensions-apiserver v0.34.0 // indirect + k8s.io/client-go v0.34.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + k8s.io/kube-openapi v0.0.0-20250905212525-66792eed8611 // indirect + k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // 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/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/plugins/wal-replica/go.sum b/plugins/wal-replica/go.sum index 903e773a..72330df6 100644 --- a/plugins/wal-replica/go.sum +++ b/plugins/wal-replica/go.sum @@ -4,14 +4,24 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudnative-pg/api v1.25.1 h1:uNjKiB0MIspUeH9l651SnFDcuflr1crB3t6LjxUCafQ= github.com/cloudnative-pg/api v1.25.1/go.mod h1:fwF5g4XkuNZqYXIeRR3AJvUfWlqWig+r2DXc5bEmw6U= +github.com/cloudnative-pg/api v1.27.0 h1:uSUkF9X/0UZu1Xn5qI33qHVmzZrDKuuyoiRlsOmSTv4= +github.com/cloudnative-pg/api v1.27.0/go.mod h1:IWyAmuirffHiw6iIGD1p18BmZNb13TK9Os/wkp8ltDg= github.com/cloudnative-pg/barman-cloud v0.1.0 h1:e/z52CehMBIh1LjZqNBJnncWJbS+1JYvRMBR8Js6Uiw= github.com/cloudnative-pg/barman-cloud v0.1.0/go.mod h1:rJUJO/f1yNckLZiVxHAyRmKY+4EPJkYRJsGbTZRJQSY= +github.com/cloudnative-pg/barman-cloud v0.3.3 h1:EEcjeV+IUivDpmyF/H/XGY1pGaKJ5LS5MYeB6wgGcak= +github.com/cloudnative-pg/barman-cloud v0.3.3/go.mod h1:5CM4MncAxAjnqxjDt0I5E/oVd7gsMLL0/o/wQ+vUSgs= github.com/cloudnative-pg/cnpg-i v0.1.0 h1:QH2xTsrODMhEEc6B25GbOYe7ZIttDmSkYvXotfU5dfs= github.com/cloudnative-pg/cnpg-i v0.1.0/go.mod h1:G28BhgUEHqrxEyyQeHz8BbpMVAsGuLhJm/tHUbDi8Sw= +github.com/cloudnative-pg/cnpg-i v0.3.0 h1:5ayNOG5x68lU70IVbHDZQrv5p+bErCJ0mqRmOpW2jjE= +github.com/cloudnative-pg/cnpg-i v0.3.0/go.mod h1:VOIWWXcJ1RyioK+elR2DGOa4cBA6K+6UQgx05aZmH+g= github.com/cloudnative-pg/cnpg-i-machinery v0.2.0 h1:htNuKirdAOYrc7Hu5mLDoOES+nKSyPaXNDLgbV5dLSI= github.com/cloudnative-pg/cnpg-i-machinery v0.2.0/go.mod h1:MHVxMMbLeCRnEM8PLWW4C2CsHqOeAU2OsrwWMKy3tPA= +github.com/cloudnative-pg/cnpg-i-machinery v0.4.0 h1:16wQt9qFFqvyxeg+9dPt8ic8dh3PRPq0jCGXVuZyjO4= +github.com/cloudnative-pg/cnpg-i-machinery v0.4.0/go.mod h1:4MCJzbCOsB7ianxlm8rqD+gDpkgVTHoTuglle/i72WA= github.com/cloudnative-pg/machinery v0.1.0 h1:tjRmsqQmsO/OlaT0uFmkEtVqgr+SGPM88cKZOHYKLBo= github.com/cloudnative-pg/machinery v0.1.0/go.mod h1:0V3vm44FaIsY+x4pm8ORry7xCC3AJiO+ebfPNxeP5Ck= +github.com/cloudnative-pg/machinery v0.3.1 h1:KtPA6EwELTUNisCMLiFYkK83GU9606rkGQhDJGPB8Yw= +github.com/cloudnative-pg/machinery v0.3.1/go.mod h1:jebuqKxZAbrRKDEEpVCIDMKW+FbWtB9Kf/hb2kMUu9o= github.com/cpuguy83/go-md2man/v2 v2.0.6/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= @@ -19,6 +29,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= @@ -27,25 +39,61 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk 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/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.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +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/go-faker/faker/v4 v4.4.1 h1:LY1jDgjVkBZWIhATCt+gkl0x9i/7wC61gZx73GTFb+Q= github.com/go-faker/faker/v4 v4.4.1/go.mod h1:HRLrjis+tYsbFtIHufEPTAIzcZiRu0rS9EYl2Ccwme4= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM= +github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA= +github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8= +github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A= +github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I= +github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8= +github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik= +github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c= +github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak= +github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90= +github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k= +github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q= +github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts= +github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0= +github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc= +github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk= +github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk= +github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc= +github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w= +github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM= +github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM= +github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w= +github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw= +github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI= +github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c= +github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 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/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -54,9 +102,13 @@ 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/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.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -66,6 +118,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 h1:kQ0NI7W1B3HwiN5gAYtY+XFItDPbLBwYRxAqbFTyDes= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0/go.mod h1:zrT2dxOAjNFPRGjTUe2Xmb4q4YdUwVvQFV6xiCSf+z0= +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/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -78,6 +132,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -86,6 +142,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 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/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -95,6 +153,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w 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/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= @@ -103,42 +163,70 @@ github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.80.1 h1:DP+PUNVOc+Bkft8a4QunLzaZ0RspWuD3tBbcPHr2PeE= github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.80.1/go.mod h1:6x4x0t9BP35g4XcjkHE9EB3RxhyfxpdpmZKd/Qyk8+M= +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.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +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.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +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.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +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.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/snorwin/jsonpatch v1.5.0 h1:0m56YSt9cHiJOn8U+OcqdPGcDQZmhPM/zsG7Dv5QQP0= github.com/snorwin/jsonpatch v1.5.0/go.mod h1:e0IDKlyFBLTFPqM0wa79dnMwjMs3XFvmKcrgCRpDqok= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/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= @@ -150,6 +238,7 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl 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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -160,25 +249,36 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 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/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= 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/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= 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 v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= 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/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= 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/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.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.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/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= +golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= 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/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -187,26 +287,40 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= +golang.org/x/oauth2 v0.31.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.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= 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.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= +golang.org/x/time v0.13.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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -219,17 +333,27 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T 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= +gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= +gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= 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/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= 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= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= @@ -239,23 +363,48 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= +k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE= +k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug= k8s.io/apiextensions-apiserver v0.32.2 h1:2YMk285jWMk2188V2AERy5yDwBYrjgWYggscghPCvV4= k8s.io/apiextensions-apiserver v0.32.2/go.mod h1:GPwf8sph7YlJT3H6aKUWtd0E+oyShk/YHWQHf/OOgCA= +k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc= +k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0= k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0= +k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA= k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94= +k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo= +k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY= 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-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg= k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7/go.mod h1:GewRfANuJ70iYzvn+i4lezLDAFzvjxZYK1gn1lWcfas= +k8s.io/kube-openapi v0.0.0-20250905212525-66792eed8611 h1:o4oKOsvSymDkZRsMAPZU7bRdwL+lPOK5VS10Dr1D6eg= +k8s.io/kube-openapi v0.0.0-20250905212525-66792eed8611/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/controller-runtime v0.20.3 h1:I6Ln8JfQjHH7JbtCD2HCYHoIzajoRxPNuvhvcDbZgkI= sigs.k8s.io/controller-runtime v0.20.3/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg= +sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY= 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/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 v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +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.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk= sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +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/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +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/plugins/wal-replica/internal/config/config.go b/plugins/wal-replica/internal/config/config.go index 72df6f91..897f7670 100644 --- a/plugins/wal-replica/internal/config/config.go +++ b/plugins/wal-replica/internal/config/config.go @@ -43,6 +43,7 @@ type Configuration struct { ReplicationHost string Synchronous SynchronousMode WalDirectory string + WalPVCSize string } // FromParameters builds a plugin configuration from the configuration parameters @@ -52,6 +53,7 @@ func FromParameters(helper *common.Plugin) *Configuration { cfg.ReplicationHost = helper.Parameters[ReplicationHostParam] cfg.Synchronous = SynchronousMode(strings.ToLower(helper.Parameters[SynchronousParam])) cfg.WalDirectory = helper.Parameters[WalDirectoryParam] + cfg.WalPVCSize = helper.Parameters[WalPVCSize] return cfg } @@ -67,6 +69,7 @@ func (c *Configuration) ToParameters() (map[string]string, error) { params[ReplicationHostParam] = c.ReplicationHost params[SynchronousParam] = string(c.Synchronous) params[WalDirectoryParam] = c.WalDirectory + params[WalPVCSize] = c.WalPVCSize return params, nil } @@ -112,4 +115,7 @@ func (c *Configuration) ApplyDefaults() { if c.Synchronous == SynchronousUnset { c.Synchronous = defaultSynchronousMode } + if c.WalPVCSize == "" { + c.WalPVCSize = "10Gi" + } } diff --git a/plugins/wal-replica/internal/identity/impl.go b/plugins/wal-replica/internal/identity/impl.go index 05f63879..d6bf1cb3 100644 --- a/plugins/wal-replica/internal/identity/impl.go +++ b/plugins/wal-replica/internal/identity/impl.go @@ -31,6 +31,13 @@ func (Implementation) GetPluginCapabilities( ) (*identity.GetPluginCapabilitiesResponse, error) { return &identity.GetPluginCapabilitiesResponse{ Capabilities: []*identity.PluginCapability{ + { + Type: &identity.PluginCapability_Service_{ + Service: &identity.PluginCapability_Service{ + Type: identity.PluginCapability_Service_TYPE_OPERATOR_SERVICE, + }, + }, + }, { Type: &identity.PluginCapability_Service_{ Service: &identity.PluginCapability_Service{ diff --git a/plugins/wal-replica/internal/operator/impl.go b/plugins/wal-replica/internal/operator/impl.go index 285fdef5..efd6d119 100644 --- a/plugins/wal-replica/internal/operator/impl.go +++ b/plugins/wal-replica/internal/operator/impl.go @@ -35,6 +35,7 @@ func (Implementation) GetCapabilities( }, }, }, + /* TODO re-add if we need status or can figure out the oscillation bug { Type: &operator.OperatorCapability_Rpc{ Rpc: &operator.OperatorCapability_RPC{ @@ -42,10 +43,14 @@ func (Implementation) GetCapabilities( }, }, }, + */ + { + Type: &operator.OperatorCapability_Rpc{ + Rpc: &operator.OperatorCapability_RPC{ + Type: operator.OperatorCapability_RPC_TYPE_MUTATE_CLUSTER, + }, + }, + }, }, }, nil } - -func (Implementation) Deregister(context.Context, *operator.DeregisterRequest) (*operator.DeregisterResponse, error) { - return &operator.DeregisterResponse{}, nil -} diff --git a/plugins/wal-replica/internal/operator/mutations.go b/plugins/wal-replica/internal/operator/mutations.go index 4d37b287..56cd0143 100644 --- a/plugins/wal-replica/internal/operator/mutations.go +++ b/plugins/wal-replica/internal/operator/mutations.go @@ -10,17 +10,19 @@ import ( "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/decoder" "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/object" "github.com/cloudnative-pg/cnpg-i/pkg/operator" + "github.com/cloudnative-pg/machinery/pkg/log" "github.com/documentdb/cnpg-i-wal-replica/internal/config" "github.com/documentdb/cnpg-i-wal-replica/pkg/metadata" ) // MutateCluster is called to mutate a cluster with the defaulting webhook. -// This function is defaulting the "imagePullPolicy" plugin parameter func (Implementation) MutateCluster( - _ context.Context, + ctx context.Context, request *operator.OperatorMutateClusterRequest, ) (*operator.OperatorMutateClusterResult, error) { + logger := log.FromContext(ctx).WithName("MutateCluster") + logger.Warning("MutateCluster hook invoked") cluster, err := decoder.DecodeClusterLenient(request.GetDefinition()) if err != nil { return nil, err @@ -33,7 +35,7 @@ func (Implementation) MutateCluster( config := config.FromParameters(helper) mutatedCluster := cluster.DeepCopy() - if helper.PluginIndex < 0 { + if helper.PluginIndex >= 0 { if mutatedCluster.Spec.Plugins[helper.PluginIndex].Parameters == nil { mutatedCluster.Spec.Plugins[helper.PluginIndex].Parameters = make(map[string]string) } @@ -43,8 +45,11 @@ func (Implementation) MutateCluster( if err != nil { return nil, err } + } else { + logger.Info("Plugin not found in the cluster, skipping mutation", "plugin", metadata.PluginName) } + logger.Info("Mutated cluster", "cluster", mutatedCluster) patch, err := object.CreatePatch(cluster, mutatedCluster) if err != nil { return nil, err diff --git a/plugins/wal-replica/internal/operator/status.go b/plugins/wal-replica/internal/operator/status.go index 6f21279e..649e5279 100644 --- a/plugins/wal-replica/internal/operator/status.go +++ b/plugins/wal-replica/internal/operator/status.go @@ -53,6 +53,11 @@ func (Implementation) SetStatusInCluster( return nil, errors.New("plugin entry not found") } + if plg.PluginIndex < 0 { + logger.Info("Plugin not being used, setting disabled status") + return clusterstatus.NewSetStatusInClusterResponseBuilder().JSONStatusResponse(Status{Enabled: false}) + } + var status Status if pluginEntry.Status != "" { if err := json.Unmarshal([]byte(pluginEntry.Status), &status); err != nil { @@ -62,13 +67,21 @@ func (Implementation) SetStatusInCluster( } } + logger.Info("debug status snapshot", + "resourceVersion", cluster.ResourceVersion, + "pluginIndex", plg.PluginIndex, + "rawPluginStatus", pluginEntry.Status, + "decodedEnabled", status.Enabled, + ) + if status.Enabled { - logger.Debug("plugin is enabled, no action taken") - return clusterstatus.NewSetStatusInClusterResponseBuilder().NoOpResponse(), nil + logger.Info("plugin is enabled, no action taken") + //return clusterstatus.NewSetStatusInClusterResponseBuilder().NoOpResponse(), nil + return clusterstatus.NewSetStatusInClusterResponseBuilder().JSONStatusResponse(Status{Enabled: true}) } // TODO uncomment this line when the `enabled` field stops alternating constantly - //logger.Info("setting enabled plugin status") + logger.Info("setting enabled plugin status") return clusterstatus.NewSetStatusInClusterResponseBuilder().JSONStatusResponse(Status{Enabled: true}) } diff --git a/plugins/wal-replica/internal/operator/validation.go b/plugins/wal-replica/internal/operator/validation.go index 65653c2f..42a0ae02 100644 --- a/plugins/wal-replica/internal/operator/validation.go +++ b/plugins/wal-replica/internal/operator/validation.go @@ -9,6 +9,7 @@ import ( "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/common" "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/decoder" "github.com/cloudnative-pg/cnpg-i/pkg/operator" + "github.com/cloudnative-pg/machinery/pkg/log" "github.com/documentdb/cnpg-i-wal-replica/internal/config" "github.com/documentdb/cnpg-i-wal-replica/pkg/metadata" @@ -17,9 +18,11 @@ import ( // ValidateClusterCreate validates a cluster that is being created, // Should validate all plugin parameters func (Implementation) ValidateClusterCreate( - _ context.Context, + ctx context.Context, request *operator.OperatorValidateClusterCreateRequest, ) (*operator.OperatorValidateClusterCreateResult, error) { + logger := log.FromContext(ctx).WithName("ValidateClusterCreate") + logger.Info("ValidateClusterCreate called") cluster, err := decoder.DecodeClusterLenient(request.GetDefinition()) if err != nil { return nil, err @@ -39,9 +42,11 @@ func (Implementation) ValidateClusterCreate( // ValidateClusterChange validates a cluster that is being changed func (Implementation) ValidateClusterChange( - _ context.Context, + ctx context.Context, request *operator.OperatorValidateClusterChangeRequest, ) (*operator.OperatorValidateClusterChangeResult, error) { + logger := log.FromContext(ctx).WithName("ValidateClusterChange") + logger.Info("ValidateClusterChange called") result := &operator.OperatorValidateClusterChangeResult{} oldCluster, err := decoder.DecodeClusterLenient(request.GetOldCluster()) diff --git a/plugins/wal-replica/internal/reconciler/replica.go b/plugins/wal-replica/internal/reconciler/replica.go index 6e289064..c7d7e50e 100644 --- a/plugins/wal-replica/internal/reconciler/replica.go +++ b/plugins/wal-replica/internal/reconciler/replica.go @@ -45,9 +45,12 @@ func CreateWalReplica( configuration := config.FromParameters(helper) + // TODO remove this once the operator functions are fixed + configuration.ApplyDefaults() + walDir := configuration.WalDirectory cmd := []string{ - "/usr/bin/postgresql/16/bin/pg_receivewal", // TODO find the actual path + "pg_receivewal", // TODO what do we do if it's not on the path? "--slot", "wal_replica", "--compress", "0", "--directory", walDir, diff --git a/plugins/wal-replica/internal/utils/doc.go b/plugins/wal-replica/internal/utils/doc.go deleted file mode 100644 index a9bff958..00000000 --- a/plugins/wal-replica/internal/utils/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -// Package utils contains methods to interact with kubernetes resources -package utils diff --git a/plugins/wal-replica/internal/utils/utils.go b/plugins/wal-replica/internal/utils/utils.go deleted file mode 100644 index 1867f3a2..00000000 --- a/plugins/wal-replica/internal/utils/utils.go +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -package utils - -import "encoding/json" - -// GetKind gets the Kubernetes object kind from its JSON representation -func GetKind(definition []byte) (string, error) { - var genericObject struct { - Kind string `json:"kind"` - } - - if err := json.Unmarshal(definition, &genericObject); err != nil { - return "", err - } - - return genericObject.Kind, nil -} diff --git a/plugins/wal-replica/kubernetes/deployment.yaml b/plugins/wal-replica/kubernetes/deployment.yaml index 1ae02479..99469564 100644 --- a/plugins/wal-replica/kubernetes/deployment.yaml +++ b/plugins/wal-replica/kubernetes/deployment.yaml @@ -24,7 +24,7 @@ spec: - containerPort: 9090 protocol: TCP args: - - plugin + - receivewal - --server-cert=/server/tls.crt - --server-key=/server/tls.key - --client-cert=/client/tls.crt From bae9bbf6e997dd3260bf9ba36d7fc66ecc75fdbc Mon Sep 17 00:00:00 2001 From: Alexander Laye Date: Tue, 9 Sep 2025 14:04:10 -0400 Subject: [PATCH 04/12] use proper user and different default values --- plugins/wal-replica/internal/config/config.go | 17 ++-- plugins/wal-replica/internal/identity/impl.go | 2 +- .../internal/operator/mutations.go | 2 +- .../wal-replica/internal/operator/status.go | 4 +- .../wal-replica/internal/reconciler/doc.go | 6 ++ .../internal/reconciler/replica.go | 79 ++++++++++++++++--- 6 files changed, 84 insertions(+), 26 deletions(-) create mode 100644 plugins/wal-replica/internal/reconciler/doc.go diff --git a/plugins/wal-replica/internal/config/config.go b/plugins/wal-replica/internal/config/config.go index 897f7670..b4ea8f76 100644 --- a/plugins/wal-replica/internal/config/config.go +++ b/plugins/wal-replica/internal/config/config.go @@ -7,6 +7,7 @@ import ( "fmt" "strings" + cnpgv1 "github.com/cloudnative-pg/api/pkg/api/v1" "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/common" "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/validation" "github.com/cloudnative-pg/cnpg-i/pkg/operator" @@ -32,8 +33,7 @@ const ( ) const ( - defaultImage = "ghcr.io/cloudnative-pg/postgresql:16" - defaultWalDir = "/var/lib/postgres/wal" + defaultWalDir = "/var/lib/postgresql/wal" defaultSynchronousMode = SynchronousInactive ) @@ -77,11 +77,6 @@ func (c *Configuration) ToParameters() (map[string]string, error) { func ValidateParams(helper *common.Plugin) []*operator.ValidationError { validationErrors := make([]*operator.ValidationError, 0) - // Must be present - if raw, present := helper.Parameters[ReplicationHostParam]; !present || raw == "" { - validationErrors = append(validationErrors, validation.BuildErrorForParameter(helper, ReplicationHostParam, "No replication host provided")) - } - // If present, must be valid if raw, present := helper.Parameters[SynchronousParam]; present && raw != "" { switch SynchronousMode(strings.ToLower(raw)) { @@ -105,9 +100,13 @@ func ValidateParams(helper *common.Plugin) []*operator.ValidationError { // applyDefaults fills the configuration with the defaults // We know that replicationhost and sync are valid already -func (c *Configuration) ApplyDefaults() { +func (c *Configuration) ApplyDefaults(cluster *cnpgv1.Cluster) { if c.Image == "" { - c.Image = defaultImage + c.Image = cluster.Status.Image + } + if c.ReplicationHost == "" { + // Only doing reads, but want to make sure we get a primary + c.ReplicationHost = cluster.Status.WriteService } if c.WalDirectory == "" { c.WalDirectory = defaultWalDir diff --git a/plugins/wal-replica/internal/identity/impl.go b/plugins/wal-replica/internal/identity/impl.go index d6bf1cb3..c0bb0288 100644 --- a/plugins/wal-replica/internal/identity/impl.go +++ b/plugins/wal-replica/internal/identity/impl.go @@ -31,7 +31,7 @@ func (Implementation) GetPluginCapabilities( ) (*identity.GetPluginCapabilitiesResponse, error) { return &identity.GetPluginCapabilitiesResponse{ Capabilities: []*identity.PluginCapability{ - { + { // TODO find out why this does nothing Type: &identity.PluginCapability_Service_{ Service: &identity.PluginCapability_Service{ Type: identity.PluginCapability_Service_TYPE_OPERATOR_SERVICE, diff --git a/plugins/wal-replica/internal/operator/mutations.go b/plugins/wal-replica/internal/operator/mutations.go index 56cd0143..68417ecf 100644 --- a/plugins/wal-replica/internal/operator/mutations.go +++ b/plugins/wal-replica/internal/operator/mutations.go @@ -39,7 +39,7 @@ func (Implementation) MutateCluster( if mutatedCluster.Spec.Plugins[helper.PluginIndex].Parameters == nil { mutatedCluster.Spec.Plugins[helper.PluginIndex].Parameters = make(map[string]string) } - config.ApplyDefaults() + config.ApplyDefaults(cluster) mutatedCluster.Spec.Plugins[helper.PluginIndex].Parameters, err = config.ToParameters() if err != nil { diff --git a/plugins/wal-replica/internal/operator/status.go b/plugins/wal-replica/internal/operator/status.go index 649e5279..1629de99 100644 --- a/plugins/wal-replica/internal/operator/status.go +++ b/plugins/wal-replica/internal/operator/status.go @@ -8,7 +8,7 @@ import ( "encoding/json" "errors" - apiv1 "github.com/cloudnative-pg/api/pkg/api/v1" + cnpgv1 "github.com/cloudnative-pg/api/pkg/api/v1" "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/clusterstatus" "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/common" "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/decoder" @@ -39,7 +39,7 @@ func (Implementation) SetStatusInCluster( plg := common.NewPlugin(*cluster, metadata.PluginName) // Find the status for our plugin - var pluginEntry *apiv1.PluginStatus + var pluginEntry *cnpgv1.PluginStatus for idx, entry := range plg.Cluster.Status.PluginStatus { if metadata.PluginName == entry.Name { pluginEntry = &plg.Cluster.Status.PluginStatus[idx] diff --git a/plugins/wal-replica/internal/reconciler/doc.go b/plugins/wal-replica/internal/reconciler/doc.go new file mode 100644 index 00000000..43652219 --- /dev/null +++ b/plugins/wal-replica/internal/reconciler/doc.go @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Package identity contains the implementation of the +// reconciler service +package reconciler diff --git a/plugins/wal-replica/internal/reconciler/replica.go b/plugins/wal-replica/internal/reconciler/replica.go index c7d7e50e..1b17a0ec 100644 --- a/plugins/wal-replica/internal/reconciler/replica.go +++ b/plugins/wal-replica/internal/reconciler/replica.go @@ -7,7 +7,7 @@ import ( "context" "fmt" - apiv1 "github.com/cloudnative-pg/api/pkg/api/v1" + cnpgv1 "github.com/cloudnative-pg/api/pkg/api/v1" "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/common" "github.com/cloudnative-pg/machinery/pkg/log" "github.com/documentdb/cnpg-i-wal-replica/internal/config" @@ -19,12 +19,11 @@ import ( "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/intstr" ) func CreateWalReplica( ctx context.Context, - cluster *apiv1.Cluster, + cluster *cnpgv1.Cluster, ) error { logger := log.FromContext(ctx).WithName("CreateWalReplica") @@ -46,7 +45,7 @@ func CreateWalReplica( configuration := config.FromParameters(helper) // TODO remove this once the operator functions are fixed - configuration.ApplyDefaults() + configuration.ApplyDefaults(cluster) walDir := configuration.WalDirectory cmd := []string{ @@ -54,10 +53,7 @@ func CreateWalReplica( "--slot", "wal_replica", "--compress", "0", "--directory", walDir, - "--host", configuration.ReplicationHost, - "--port", "5432", - "--username", "postgres", - "--no-password", + "--dbname", GetConnectionString(configuration.ReplicationHost), } // TODO have a real check here @@ -75,12 +71,24 @@ func CreateWalReplica( existingPVC := &corev1.PersistentVolumeClaim{} err := client.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: namespace}, existingPVC) if err != nil && errors.IsNotFound(err) { - log.Info("WAL replica PVC not found. Creating a new WAL replica PVC") + logger.Info("WAL replica PVC not found. Creating a new WAL replica PVC") walReplicaPVC := &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: deploymentName, Namespace: namespace, + Labels: map[string]string{ + "app": deploymentName, + "cnpg.io/cluster": cluster.Name, + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: cluster.APIVersion, + Kind: cluster.Kind, + Name: cluster.Name, + UID: cluster.UID, + }, + }, }, Spec: corev1.PersistentVolumeClaimSpec{ AccessModes: []corev1.PersistentVolumeAccessMode{ @@ -114,6 +122,14 @@ func CreateWalReplica( "app": deploymentName, "cnpg.io/cluster": cluster.Name, }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: cluster.APIVersion, + Kind: cluster.Kind, + Name: cluster.Name, + UID: cluster.UID, + }, + }, }, Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": deploymentName}}, @@ -129,6 +145,16 @@ func CreateWalReplica( Name: deploymentName, MountPath: walDir, }, + { + Name: "ca", + MountPath: "/var/lib/postgresql/rootcert", + ReadOnly: true, + }, + { + Name: "tls", + MountPath: "/var/lib/postgresql/cert", + ReadOnly: true, + }, }, }}, Volumes: []corev1.Volume{ @@ -140,6 +166,24 @@ func CreateWalReplica( }, }, }, + { + Name: "ca", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: cluster.Status.Certificates.ServerCASecret, + DefaultMode: int32Ptr(0600), + }, + }, + }, + { + Name: "tls", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: cluster.Status.Certificates.ReplicationTLSSecret, + DefaultMode: int32Ptr(0600), + }, + }, + }, }, SecurityContext: &corev1.PodSecurityContext{ RunAsUser: int64Ptr(105), @@ -151,13 +195,10 @@ func CreateWalReplica( }, }, } - // optional service for metrics - svc := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: deploymentName, Namespace: namespace, Labels: map[string]string{"app": deploymentName}}, Spec: corev1.ServiceSpec{Selector: map[string]string{"app": deploymentName}, Ports: []corev1.ServicePort{{Name: "metrics", Port: 9187, TargetPort: intstr.FromInt(9187)}}}} if createErr := client.Create(ctx, dep); createErr != nil { logger.Error(createErr, "creating wal receiver deployment") return createErr } - _ = client.Create(ctx, svc) // ignore error if exists logger.Info("created wal receiver deployment", "name", deploymentName) } else { // TODO handle patch @@ -165,11 +206,23 @@ func CreateWalReplica( return nil } + +func GetConnectionString(host string) string { + return fmt.Sprintf("postgres://%s@%s/postgres?sslmode=verify-full&sslrootcert=%s&sslcert=%s&sslkey=%s", + "streaming_replica", // user + host, + "/var/lib/postgresql/rootcert/ca.crt", // root cert + "/var/lib/postgresql/cert/tls.crt", // cert + "/var/lib/postgresql/cert/tls.key") // key +} func int64Ptr(i int64) *int64 { return &i } +func int32Ptr(i int32) *int32 { + return &i +} -func IsPrimaryCluster(cluster *apiv1.Cluster) bool { +func IsPrimaryCluster(cluster *cnpgv1.Cluster) bool { // TODO implement return true } From d32bd56ddf9536db5bfab6e12b08012ccc0bb359 Mon Sep 17 00:00:00 2001 From: Alexander Laye Date: Tue, 9 Sep 2025 20:51:44 -0400 Subject: [PATCH 05/12] Add examples and start making custom image --- .../doc/examples/cluster-example.yaml | 11 +++++++++-- .../wal-replica/internal/reconciler/impl.go | 4 ++-- .../wal-replica/internal/reconciler/replica.go | 3 ++- .../wal-replica/pg-receivewal-min/Dockerfile | 18 ++++++++++++++++++ plugins/wal-replica/scripts/build.sh | 2 +- 5 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 plugins/wal-replica/pg-receivewal-min/Dockerfile diff --git a/plugins/wal-replica/doc/examples/cluster-example.yaml b/plugins/wal-replica/doc/examples/cluster-example.yaml index f592de4b..258eeaef 100644 --- a/plugins/wal-replica/doc/examples/cluster-example.yaml +++ b/plugins/wal-replica/doc/examples/cluster-example.yaml @@ -7,8 +7,15 @@ spec: plugins: - name: cnpg-i-wal-replica.documentdb.io - parameters: - replicationHost: cluster-example-rw + + replicationSlots: + synchronizeReplicas: + enabled: true + + bootstrap: + initdb: + postInitSQL: + "select * from pg_create_physical_replication_slot('wal_replica'); storage: size: 1Gi diff --git a/plugins/wal-replica/internal/reconciler/impl.go b/plugins/wal-replica/internal/reconciler/impl.go index a35eec08..e94cd99a 100644 --- a/plugins/wal-replica/internal/reconciler/impl.go +++ b/plugins/wal-replica/internal/reconciler/impl.go @@ -7,7 +7,7 @@ import ( "context" "encoding/json" - apiv1 "github.com/cloudnative-pg/api/pkg/api/v1" + cnpgv1 "github.com/cloudnative-pg/api/pkg/api/v1" "github.com/cloudnative-pg/cnpg-i/pkg/reconciler" "github.com/cloudnative-pg/machinery/pkg/log" ) @@ -33,7 +33,7 @@ func (Implementation) GetCapabilities( func (Implementation) Post(ctx context.Context, req *reconciler.ReconcilerHooksRequest) (*reconciler.ReconcilerHooksResult, error) { logger := log.FromContext(ctx).WithName("PostReconcilerHook") - cluster := &apiv1.Cluster{} + cluster := &cnpgv1.Cluster{} if err := json.Unmarshal(req.GetResourceDefinition(), cluster); err != nil { logger.Error(err, "while decoding the cluster") return nil, err diff --git a/plugins/wal-replica/internal/reconciler/replica.go b/plugins/wal-replica/internal/reconciler/replica.go index 1b17a0ec..7890f890 100644 --- a/plugins/wal-replica/internal/reconciler/replica.go +++ b/plugins/wal-replica/internal/reconciler/replica.go @@ -66,7 +66,6 @@ func CreateWalReplica( cmd = append(cmd, "--synchronous") } - // Create a pVC // Needs a PVC to store the wal data existingPVC := &corev1.PersistentVolumeClaim{} err := client.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: namespace}, existingPVC) @@ -110,6 +109,8 @@ func CreateWalReplica( return err } + // Create replica slot + // Create or patch Deployment existing := &appsv1.Deployment{} err = client.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: namespace}, existing) diff --git a/plugins/wal-replica/pg-receivewal-min/Dockerfile b/plugins/wal-replica/pg-receivewal-min/Dockerfile new file mode 100644 index 00000000..8ebe6345 --- /dev/null +++ b/plugins/wal-replica/pg-receivewal-min/Dockerfile @@ -0,0 +1,18 @@ +FROM gcr.io/distroless/base-debian12:latest + +COPY --from=postgresql:17 /usr/share/postgresql-common/ /bin + +ENV PATH="$PATH:/bin/postgresql-common" + +ENTRYPOINT [ + "pg_wrapper" + --slot + wal_replica + --compress + 0 + --directory + /var/lib/postgresql/wal + --dbname + postgres://streaming_replica@cluster-example-rw/postgres?sslmode=verify-full&sslrootcert=/var/lib/postgresql/rootcert/ca.crt&sslcert=/var/lib/postgresql/cert/tls.crt&sslkey=/var/lib/postgresql/cert/tls.key + --verbose + ] diff --git a/plugins/wal-replica/scripts/build.sh b/plugins/wal-replica/scripts/build.sh index df30b287..aeaa391e 100755 --- a/plugins/wal-replica/scripts/build.sh +++ b/plugins/wal-replica/scripts/build.sh @@ -3,4 +3,4 @@ cd "$(dirname "$0")/.." || exit # Compile the plugin -CGO_ENABLED=0 go build -o bin/cnpg-i-wal-replica main.go +CGO_ENABLED=0 go build -gcflags="all=-N -l" -o bin/cnpg-i-wal-replica main.go From e8534eca6cb446639b146e3dc8d5631284a9ee9b Mon Sep 17 00:00:00 2001 From: Alexander Laye Date: Wed, 10 Sep 2025 13:03:33 -0400 Subject: [PATCH 06/12] distroless image first go --- .../wal-replica/pg-receivewal-min/Dockerfile | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/plugins/wal-replica/pg-receivewal-min/Dockerfile b/plugins/wal-replica/pg-receivewal-min/Dockerfile index 8ebe6345..b252720f 100644 --- a/plugins/wal-replica/pg-receivewal-min/Dockerfile +++ b/plugins/wal-replica/pg-receivewal-min/Dockerfile @@ -1,18 +1,30 @@ -FROM gcr.io/distroless/base-debian12:latest +FROM postgres:17 AS pg +FROM gcr.io/distroless/base:latest + +COPY --from=pg /usr/lib/postgresql/17/bin/pg_receivewal /bin/pg_receivewal + +# Copy libraries +COPY --from=pg /usr/lib/x86_64-linux-gnu/libpq.so.* /usr/lib/x86_64-linux-gnu +COPY --from=pg /usr/lib/x86_64-linux-gnu/libzstd.so.* /usr/lib/x86_64-linux-gnu +COPY --from=pg /usr/lib/x86_64-linux-gnu/liblz4.so.* /usr/lib/x86_64-linux-gnu +COPY --from=pg /usr/lib/x86_64-linux-gnu/libz.so.* /usr/lib/x86_64-linux-gnu +COPY --from=pg /usr/lib/x86_64-linux-gnu/libgssapi_krb5.so.* /usr/lib/x86_64-linux-gnu +COPY --from=pg /usr/lib/x86_64-linux-gnu/libldap.so.* /usr/lib/x86_64-linux-gnu +COPY --from=pg /usr/lib/x86_64-linux-gnu/libxxhash.so.* /usr/lib/x86_64-linux-gnu +COPY --from=pg /usr/lib/x86_64-linux-gnu/libkrb5.so.* /usr/lib/x86_64-linux-gnu +COPY --from=pg /usr/lib/x86_64-linux-gnu/libk5crypto.so.* /usr/lib/x86_64-linux-gnu +COPY --from=pg /usr/lib/x86_64-linux-gnu/libcom_err.so.* /usr/lib/x86_64-linux-gnu +COPY --from=pg /usr/lib/x86_64-linux-gnu/libkrb5support.so.* /usr/lib/x86_64-linux-gnu +COPY --from=pg /usr/lib/x86_64-linux-gnu/liblber.so.* /usr/lib/x86_64-linux-gnu +COPY --from=pg /usr/lib/x86_64-linux-gnu/libsasl2.so.* /usr/lib/x86_64-linux-gnu +COPY --from=pg /usr/lib/x86_64-linux-gnu/libkeyutils.so.* /usr/lib/x86_64-linux-gnu -COPY --from=postgresql:17 /usr/share/postgresql-common/ /bin ENV PATH="$PATH:/bin/postgresql-common" -ENTRYPOINT [ - "pg_wrapper" - --slot - wal_replica - --compress - 0 - --directory - /var/lib/postgresql/wal - --dbname - postgres://streaming_replica@cluster-example-rw/postgres?sslmode=verify-full&sslrootcert=/var/lib/postgresql/rootcert/ca.crt&sslcert=/var/lib/postgresql/cert/tls.crt&sslkey=/var/lib/postgresql/cert/tls.key - --verbose - ] +ENTRYPOINT ["/bin/pg_receivewal",\ + "--slot", "wal_replica",\ + "--compress", "0",\ + "--directory", "/var/lib/postgresql/wal",\ + "--dbname", "postgres://streaming_replica@cluster-example-rw/postgres?sslmode=verify-full&sslrootcert=/var/lib/postgresql/rootcert/ca.crt&sslcert=/var/lib/postgresql/cert/tls.crt&sslkey=/var/lib/postgresql/cert/tls.key",\ + "--verbose"] From 4469daf14d4d65713c04b342c9d1cad380333895 Mon Sep 17 00:00:00 2001 From: Alexander Laye Date: Wed, 10 Sep 2025 16:16:29 -0400 Subject: [PATCH 07/12] add wal slot creation to the plugin --- .../doc/examples/cluster-example.yaml | 5 -- .../internal/reconciler/replica.go | 70 ++++++++++++------- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/plugins/wal-replica/doc/examples/cluster-example.yaml b/plugins/wal-replica/doc/examples/cluster-example.yaml index 258eeaef..7961a303 100644 --- a/plugins/wal-replica/doc/examples/cluster-example.yaml +++ b/plugins/wal-replica/doc/examples/cluster-example.yaml @@ -12,11 +12,6 @@ spec: synchronizeReplicas: enabled: true - bootstrap: - initdb: - postInitSQL: - "select * from pg_create_physical_replication_slot('wal_replica'); - storage: size: 1Gi diff --git a/plugins/wal-replica/internal/reconciler/replica.go b/plugins/wal-replica/internal/reconciler/replica.go index 7890f890..7d11630b 100644 --- a/plugins/wal-replica/internal/reconciler/replica.go +++ b/plugins/wal-replica/internal/reconciler/replica.go @@ -6,6 +6,7 @@ package reconciler import ( "context" "fmt" + "strings" cnpgv1 "github.com/cloudnative-pg/api/pkg/api/v1" "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/common" @@ -47,25 +48,6 @@ func CreateWalReplica( // TODO remove this once the operator functions are fixed configuration.ApplyDefaults(cluster) - walDir := configuration.WalDirectory - cmd := []string{ - "pg_receivewal", // TODO what do we do if it's not on the path? - "--slot", "wal_replica", - "--compress", "0", - "--directory", walDir, - "--dbname", GetConnectionString(configuration.ReplicationHost), - } - - // TODO have a real check here - if true { - cmd = append(cmd, "--verbose") - } - - // Add synchronous flag if requested - if configuration.Synchronous == config.SynchronousActive { - cmd = append(cmd, "--synchronous") - } - // Needs a PVC to store the wal data existingPVC := &corev1.PersistentVolumeClaim{} err := client.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: namespace}, existingPVC) @@ -109,7 +91,17 @@ func CreateWalReplica( return err } - // Create replica slot + walDir := configuration.WalDirectory + + // Put the strings together so they run as separate commands, then rewrap + // them in a single arg + args := []string{ + strings.Join([]string{ + GetCommandForWalReceiver(configuration, walDir, true), + "&&", + GetCommandForWalReceiver(configuration, walDir, false), + }, " "), + } // Create or patch Deployment existing := &appsv1.Deployment{} @@ -138,9 +130,10 @@ func CreateWalReplica( ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": deploymentName}}, Spec: corev1.PodSpec{ Containers: []corev1.Container{{ - Name: "wal-receiver", - Image: configuration.Image, - Args: cmd, + Name: "wal-receiver", + Image: configuration.Image, + Command: []string{"/bin/bash", "-c"}, + Args: args, VolumeMounts: []corev1.VolumeMount{ { Name: deploymentName, @@ -208,14 +201,39 @@ func CreateWalReplica( return nil } -func GetConnectionString(host string) string { - return fmt.Sprintf("postgres://%s@%s/postgres?sslmode=verify-full&sslrootcert=%s&sslcert=%s&sslkey=%s", +// TODO change this to just use a custom image that creates the slot and the replica +func GetCommandForWalReceiver(configuration *config.Configuration, walDir string, createSlot bool) string { + connectionString := fmt.Sprintf("postgres://%s@%s/postgres?sslmode=verify-full&sslrootcert=%s&sslcert=%s&sslkey=%s", "streaming_replica", // user - host, + configuration.ReplicationHost, "/var/lib/postgresql/rootcert/ca.crt", // root cert "/var/lib/postgresql/cert/tls.crt", // cert "/var/lib/postgresql/cert/tls.key") // key + createSlotFlag := "" + if createSlot { + createSlotFlag = "--create-slot --if-not-exists" + } + + // TODO have a real check here + verboseFlag := "" + if true { + verboseFlag = "--verbose" + } + + synchronousFlag := "" + if configuration.Synchronous == config.SynchronousActive { + synchronousFlag = "--synchronous" + } + + return fmt.Sprintf("pg_receivewal --slot wal_replica --compress 0 --directory %s --dbname \"%s\" %s %s %s", + walDir, + connectionString, + createSlotFlag, + verboseFlag, + synchronousFlag, + ) } + func int64Ptr(i int64) *int64 { return &i } From fa8b988befc389ec25f99e05995ab8120d7f55b1 Mon Sep 17 00:00:00 2001 From: Alexander Laye Date: Thu, 11 Sep 2025 09:35:52 -0400 Subject: [PATCH 08/12] remove non-working dockerfile --- plugins/wal-replica/internal/config/config.go | 2 +- .../wal-replica/pg-receivewal-min/Dockerfile | 30 ------------------- 2 files changed, 1 insertion(+), 31 deletions(-) delete mode 100644 plugins/wal-replica/pg-receivewal-min/Dockerfile diff --git a/plugins/wal-replica/internal/config/config.go b/plugins/wal-replica/internal/config/config.go index b4ea8f76..e9ee13a3 100644 --- a/plugins/wal-replica/internal/config/config.go +++ b/plugins/wal-replica/internal/config/config.go @@ -17,7 +17,7 @@ import ( // Plugin parameter keys const ( ImageParam = "image" // string - ReplicationHostParam = "replicationHost" // Required: primary host + ReplicationHostParam = "replicationHost" // primary host SynchronousParam = "synchronous" // enum: Active, Inactive, Unset WalDirectoryParam = "walDirectory" // directory where WAL is stored WalPVCSize = "walPVCSize" // Size of the PVC for WAL storage diff --git a/plugins/wal-replica/pg-receivewal-min/Dockerfile b/plugins/wal-replica/pg-receivewal-min/Dockerfile deleted file mode 100644 index b252720f..00000000 --- a/plugins/wal-replica/pg-receivewal-min/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -FROM postgres:17 AS pg -FROM gcr.io/distroless/base:latest - -COPY --from=pg /usr/lib/postgresql/17/bin/pg_receivewal /bin/pg_receivewal - -# Copy libraries -COPY --from=pg /usr/lib/x86_64-linux-gnu/libpq.so.* /usr/lib/x86_64-linux-gnu -COPY --from=pg /usr/lib/x86_64-linux-gnu/libzstd.so.* /usr/lib/x86_64-linux-gnu -COPY --from=pg /usr/lib/x86_64-linux-gnu/liblz4.so.* /usr/lib/x86_64-linux-gnu -COPY --from=pg /usr/lib/x86_64-linux-gnu/libz.so.* /usr/lib/x86_64-linux-gnu -COPY --from=pg /usr/lib/x86_64-linux-gnu/libgssapi_krb5.so.* /usr/lib/x86_64-linux-gnu -COPY --from=pg /usr/lib/x86_64-linux-gnu/libldap.so.* /usr/lib/x86_64-linux-gnu -COPY --from=pg /usr/lib/x86_64-linux-gnu/libxxhash.so.* /usr/lib/x86_64-linux-gnu -COPY --from=pg /usr/lib/x86_64-linux-gnu/libkrb5.so.* /usr/lib/x86_64-linux-gnu -COPY --from=pg /usr/lib/x86_64-linux-gnu/libk5crypto.so.* /usr/lib/x86_64-linux-gnu -COPY --from=pg /usr/lib/x86_64-linux-gnu/libcom_err.so.* /usr/lib/x86_64-linux-gnu -COPY --from=pg /usr/lib/x86_64-linux-gnu/libkrb5support.so.* /usr/lib/x86_64-linux-gnu -COPY --from=pg /usr/lib/x86_64-linux-gnu/liblber.so.* /usr/lib/x86_64-linux-gnu -COPY --from=pg /usr/lib/x86_64-linux-gnu/libsasl2.so.* /usr/lib/x86_64-linux-gnu -COPY --from=pg /usr/lib/x86_64-linux-gnu/libkeyutils.so.* /usr/lib/x86_64-linux-gnu - - -ENV PATH="$PATH:/bin/postgresql-common" - -ENTRYPOINT ["/bin/pg_receivewal",\ - "--slot", "wal_replica",\ - "--compress", "0",\ - "--directory", "/var/lib/postgresql/wal",\ - "--dbname", "postgres://streaming_replica@cluster-example-rw/postgres?sslmode=verify-full&sslrootcert=/var/lib/postgresql/rootcert/ca.crt&sslcert=/var/lib/postgresql/cert/tls.crt&sslkey=/var/lib/postgresql/cert/tls.key",\ - "--verbose"] From ea15d4d549fe7d2c3304d28eea8a84d207cdb685 Mon Sep 17 00:00:00 2001 From: Alexander Laye Date: Tue, 30 Sep 2025 09:20:57 -0400 Subject: [PATCH 09/12] Update docs --- plugins/wal-replica/README.md | 197 ++++++++-- plugins/wal-replica/doc/development.md | 500 ++++++++++++++++++------ plugins/wal-replica/pkg/metadata/doc.go | 2 +- 3 files changed, 551 insertions(+), 148 deletions(-) diff --git a/plugins/wal-replica/README.md b/plugins/wal-replica/README.md index d9b11a57..1967f50b 100644 --- a/plugins/wal-replica/README.md +++ b/plugins/wal-replica/README.md @@ -1,49 +1,190 @@ -# WAL Receiver Pod Manager (CNPG-I Plugin) +# WAL Replica Pod Manager (CNPG-I Plugin) -This plugin adds an optional standalone WAL receiver (pg_receivewal) Pod/Deployment -alongside a [CloudNativePG](https://github.com/cloudnative-pg/cloudnative-pg/) Cluster. -It reconciles a Deployment named -`-wal-receiver` that continuously streams WAL files from the primary -cluster using `pg_receivewal`, supporting synchronous mode. +This plugin creates a standalone WAL receiver deployment alongside a [CloudNativePG](https://github.com/cloudnative-pg/cloudnative-pg/) cluster. It automatically provisions a Deployment named `-wal-receiver` that continuously streams Write-Ahead Log (WAL) files from the primary PostgreSQL cluster using `pg_receivewal`, with support for both synchronous and asynchronous replication modes. -## Parameters +## Features -Add the plugin in the Cluster spec (example): +- **Automated WAL Streaming**: Continuously receives and stores WAL files from the primary cluster +- **Persistent Storage**: Automatically creates and manages a PersistentVolumeClaim for WAL storage +- **TLS Security**: Uses cluster certificates for secure replication connections +- **Replication Slot Management**: Automatically creates and manages a dedicated replication slot (`wal_replica`) +- **Synchronous Replication Support**: Configurable synchronous/asynchronous replication modes +- **Cluster Lifecycle Management**: Automatically manages resources with proper owner references + +## Configuration + +Add the plugin to your Cluster specification: ```yaml +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: my-cluster spec: - plugins: - - name: cnpg-i-wal-replica.documentdb.io - parameters: - image: "ghcr.io/cloudnative-pg/postgresql:16" - replicationHost: cluster-name-rw - synchronous: "enabled" # optional (default true) - walDirectory: /var/lib/wal # optional (default /var/lib/wal) + instances: 3 + + plugins: + - name: cnpg-i-wal-replica.documentdb.io + parameters: + image: "ghcr.io/cloudnative-pg/postgresql:16" + replicationHost: "my-cluster-rw" + synchronous: "active" + walDirectory: "/var/lib/postgresql/wal" + walPVCSize: "20Gi" + + replicationSlots: + synchronizeReplicas: + enabled: true + + storage: + size: 10Gi ``` +### Parameters + | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| image | string | ghcr.io/cloudnative-pg/postgresql:16 | Image providing pg_receivewal | -| replicationHost | string | -rw | Host to connect for streaming | -| synchronous | bool | true | Add --synchronous flag to pg_receivewal | -| walDirectory | string | /var/lib/wal | Local directory to store WAL | +| `image` | string | Cluster status image | Container image providing `pg_receivewal` binary | +| `replicationHost` | string | `-rw` | Primary host endpoint for WAL streaming | +| `synchronous` | string | `inactive` | Replication mode: `active` (synchronous) or `inactive` (asynchronous) | +| `walDirectory` | string | `/var/lib/postgresql/wal` | Directory path for storing received WAL files | +| `walPVCSize` | string | `10Gi` | Size of the PersistentVolumeClaim for WAL storage | + +#### Synchronous Modes + +- **`active`**: Enables synchronous replication with `--synchronous` flag +- **`inactive`**: Standard asynchronous replication (default) + +## Architecture + +The plugin creates the following Kubernetes resources: + +1. **Deployment**: `-wal-receiver` + - Single replica pod running `pg_receivewal` + - Configured with proper security context (user: 105, group: 103) + - Automatic restart policy for high availability + +2. **PersistentVolumeClaim**: `-wal-receiver` + - Stores received WAL files persistently + - Uses `ReadWriteOnce` access mode + - Configurable size via `walPVCSize` parameter + +3. **Volume Mounts**: + - WAL storage: Mounted at configured `walDirectory` + - TLS certificates: Mounted from cluster certificate secrets + - CA certificates: Mounted for SSL verification -The Deployment exposes a metrics port (9187) and creates a Service with the same name. +## Security -## Build +The plugin implements comprehensive security measures: + +- **TLS Encryption**: All replication connections use SSL/TLS +- **Certificate Management**: Automatically mounts cluster CA and client certificates +- **User Privileges**: Runs with dedicated PostgreSQL user and group IDs +- **Connection Authentication**: Uses `streaming_replica` user with certificate-based auth + +## Prerequisites + +- CloudNativePG operator installed and running +- CNPG-I (CloudNativePG Interface) framework deployed +- Cluster with enabled replication slots synchronization +- Sufficient storage for WAL files retention + +## Installation + +### Building from Source ```bash +# Clone the repository +git clone https://github.com/documentdb/cnpg-i-wal-replica +cd cnpg-i-wal-replica + +# Build the binary go build -o bin/cnpg-i-wal-replica main.go ``` -## Status +### Using Docker + +```bash +# Build container image +docker build -t cnpg-i-wal-replica:latest . +``` + +### Deployment Scripts + +```bash +# Make scripts executable +chmod +x scripts/build.sh scripts/run.sh + +# Build and run +./scripts/build.sh +./scripts/run.sh +``` + +## Monitoring and Observability + +The WAL receiver pod provides verbose logging when enabled, including: + +- Connection status to primary cluster +- WAL file reception progress +- Replication slot status +- SSL/TLS connection details + +## Examples + +See the `doc/examples/` directory for complete cluster configurations: + +- [`cluster-example.yaml`](doc/examples/cluster-example.yaml): Basic configuration +- [`cluster-example-no-parameters.yaml`](doc/examples/cluster-example-no-parameters.yaml): Default settings +- [`cluster-example-with-mistake.yaml`](doc/examples/cluster-example-with-mistake.yaml): Common configuration errors + +## Development + +### Project Structure + +``` +├── cmd/plugin/ # Plugin command-line interface +├── internal/ +│ ├── config/ # Configuration management +│ ├── identity/ # Plugin identity and metadata +│ ├── k8sclient/ # Kubernetes client utilities +│ ├── operator/ # Operator implementations +│ └── reconciler/ # Resource reconciliation logic +├── kubernetes/ # Kubernetes manifests +├── pkg/metadata/ # Plugin metadata and constants +└── scripts/ # Build and deployment scripts +``` + +### Running Tests + +```bash +go test ./... +``` + +See [`doc/development.md`](doc/development.md) for detailed development guidelines. + +## Limitations and Future Enhancements + +### Current Limitations + +- Fixed compression level (disabled: `--compress 0`) +- No built-in WAL retention/cleanup policies +- Limited resource configuration options + +### Planned Enhancements + +- [ ] Configurable resource requests and limits +- [ ] WAL retention and garbage collection policies +- [ ] Health checks and readiness probes +- [ ] Metrics exposure for monitoring integration +- [ ] Multi-zone/region WAL archiving support +- [ ] Backup integration with existing CNPG backup strategies + +## License -The plugin status reflects only whether it is enabled. +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. -## Future Work +## Contributing -* Add PVC / volume configuration for WAL directory -* Expose resource requests/limits and security context -* Garbage collection / retention policy for archived WAL -* Liveness/readiness refinements +Contributions are welcome! Please read [CONTRIBUTING.md](../../CONTRIBUTING.md) for guidelines on how to contribute to this project. diff --git a/plugins/wal-replica/doc/development.md b/plugins/wal-replica/doc/development.md index b98e9cd1..e8887f47 100644 --- a/plugins/wal-replica/doc/development.md +++ b/plugins/wal-replica/doc/development.md @@ -1,180 +1,442 @@ -# Plugin Development +# WAL Replica Plugin Development Guide -This section of the documentation illustrates the CNPG-I capabilities used by -the wal-replica plugin, how the plugin implementation uses them, and how -developers can build and deploy the plugin. +This document provides comprehensive guidance for developers working with the WAL Replica plugin for CloudNativePG. It covers the CNPG-I framework capabilities used, implementation details, and development workflows. -## Concepts +## Overview -### Identity +The WAL Replica plugin (`cnpg-i-wal-replica.documentdb.io`) is built using the CloudNativePG Interface (CNPG-I) framework, which provides a plugin architecture for extending CloudNativePG clusters with custom functionality. -The Identity interface defines the features supported by the plugin and is the -only interface that must always be implemented. +### Plugin Metadata -This information is essential for the operator to discover the plugin's -capabilities during startup. +```go +// From pkg/metadata/doc.go +const PluginName = "cnpg-i-wal-replica.documentdb.io" +Version: "0.1.0" +DisplayName: "WAL Replica Pod Manager" +License: "MIT" +Maturity: "alpha" +``` + +## CNPG-I Framework Concepts -The Identity interface provides: +### Identity Interface -- A mechanism for plugins to report readiness probes. Readiness is a - prerequisite for receiving events, and plugins are expected to always report - the most accurate readiness data available. -- The capabilities reported by the plugin, which determine the subsequent calls - the plugin will receive. -- Metadata about the plugin. +The Identity interface is fundamental to all CNPG-I plugins and defines: -[API reference](https://github.com/cloudnative-pg/cnpg-i/blob/main/proto/identity.proto) +- **Plugin Metadata**: Name, version, description, and licensing information +- **Capabilities**: Which CNPG-I services the plugin implements +- **Readiness**: Health check mechanism for the plugin -### Capabilities +The identity implementation is located in [`internal/identity/impl.go`](../internal/identity/impl.go). -This plugin implements the Operator and the Lifecycle capabilities. +**Key Methods:** +- `GetPluginMetadata()`: Returns plugin information from `pkg/metadata` +- `GetPluginCapabilities()`: Declares supported services (Operator + Reconciler) +- `Probe()`: Always returns ready (stateless plugin) -#### Operator +[CNPG-I Identity API Reference](https://github.com/cloudnative-pg/cnpg-i/blob/main/proto/identity.proto) -This feature enables the plugin to receive events about the cluster creation and -mutations, this is defined by the following +### Implemented Capabilities -``` proto -// ValidateCreate improves the behavior of the validating webhook that -// is called on creation of the Cluster resources -rpc ValidateClusterCreate(OperatorValidateClusterCreateRequest) returns (OperatorValidateClusterCreateResult) {} +This plugin implements two core CNPG-I capabilities: -// ValidateClusterChange improves the behavior of the validating webhook of -// is called on updates of the Cluster resources -rpc ValidateClusterChange(OperatorValidateClusterChangeRequest) returns (OperatorValidateClusterChangeResult) {} +#### 1. Operator Interface -// MutateCluster fills in the defaults inside a Cluster resource -rpc MutateCluster(OperatorMutateClusterRequest) returns (OperatorMutateClusterResult) {} +Provides cluster-level validation and mutation capabilities through webhooks: + +```go +// From internal/operator/ +rpc ValidateClusterCreate(OperatorValidateClusterCreateRequest) returns (OperatorValidateClusterCreateResult) +rpc ValidateClusterChange(OperatorValidateClusterChangeRequest) returns (OperatorValidateClusterChangeResult) +rpc MutateCluster(OperatorMutateClusterRequest) returns (OperatorMutateClusterResult) ``` -This interface allows plugins to implement important features like: +**Implementation Features:** +- **Parameter Validation**: Validates `synchronous`, `walPVCSize`, and other plugin parameters +- **Default Application**: Sets default values for image, replication host, WAL directory +- **Configuration Parsing**: Converts plugin parameters to typed configuration objects -1. validating the cluster manifest during the creation and mutations - (it is expected that the plugin validate the parameters assigned to their - configuration). +See [`internal/operator/validation.go`](../internal/operator/validation.go) and [`internal/operator/mutations.go`](../internal/operator/mutations.go). -2. mutating the cluster object before it is submitted to kubernetes API server, - for example to set default values for the plugin parameters. +#### 2. Reconciler Hooks Interface -[API reference](https://github.com/cloudnative-pg/cnpg-i/blob/main/proto/operator.proto) +Enables resource reconciliation and custom Kubernetes resource management: -The wal-replica plugin is using this to validate used-defined parameters, and to -set default values for the labels and annotations applied by the plugin if not -specified by the user. +```go +// From internal/reconciler/ +rpc ReconcilerHook(ReconcilerHookRequest) returns (ReconcilerHookResponse) +``` -#### Lifecycle +**Core Functionality:** +- **WAL Receiver Deployment**: Creates and manages the `-wal-receiver` deployment +- **PVC Management**: Provisions persistent storage for WAL files +- **TLS Configuration**: Sets up certificate-based authentication +- **Resource Lifecycle**: Handles creation, updates, and cleanup with owner references -This feature enables the plugin to receive events and create patches for -Kubernetes resources `before` they are submitted to the API server. +See [`internal/reconciler/replica.go`](../internal/reconciler/replica.go) for the main implementation. -To use this feature, the plugin must specify the resource and operation it wants -to be notified of. +[CNPG-I Operator API Reference](https://github.com/cloudnative-pg/cnpg-i/blob/main/proto/operator.proto) -Some examples of what it can be achieved through the lifecycle: +## Architecture Deep Dive -- add volume, volume mounts, sidecar containers, labels, annotations to pods, - especially necessary when implementing custom backup solutions -- modify any resource with some annotations or labels -- add/remove finalizers +### Configuration Management -[API reference](https://github.com/cloudnative-pg/cnpg-i/blob/main/proto/operator_lifecycle.proto): +The plugin uses a layered configuration approach: -The wal-replica plugin is using this to add labels, annotations and a sidecar -to the pods. +```go +// From internal/config/config.go +type Configuration struct { + Image string // Container image for pg_receivewal + ReplicationHost string // Primary cluster endpoint + Synchronous SynchronousMode // active/inactive replication mode + WalDirectory string // WAL storage path + WalPVCSize string // Storage size for PVC +} +``` -## Implementation +**Configuration Flow:** +1. Raw parameters from Cluster spec +2. Validation using `ValidateParams()` +3. Type conversion via `FromParameters()` +4. Default application with `ApplyDefaults()` + +### Resource Reconciliation + +The plugin creates and manages several Kubernetes resources: + +#### WAL Receiver Deployment + +```yaml +# Generated deployment structure +apiVersion: apps/v1 +kind: Deployment +metadata: + name: -wal-receiver + ownerReferences: [] +spec: + containers: + - name: wal-receiver + image: + command: ["/bin/bash", "-c"] + args: [""] + volumeMounts: + - name: wal-storage + mountPath: + - name: ca + mountPath: /var/lib/postgresql/rootcert + - name: tls + mountPath: /var/lib/postgresql/cert +``` -### Identity +#### pg_receivewal Command Construction -1. Define a struct inside the `internal/identity` package that implements - the `pluginhelper.IdentityServer` interface. +The plugin builds sophisticated `pg_receivewal` commands: -2. Implement the following methods: +```bash +# Two-phase execution: +# 1. Create replication slot (if needed) +pg_receivewal --slot wal_replica --create-slot --if-not-exists --directory /path/to/wal --dbname "postgres://streaming_replica@host/postgres?sslmode=verify-full&..." - - `GetPluginMetadata`: return human-readable information about the plugin. - - `GetPluginCapabilities`: specify the features supported by the plugin. In - the wal-replica example, the - `PluginCapability_Service_TYPE_LIFECYCLE_SERVICE` is defined in the - corresponding Go [file](../internal/lifecycle/lifecycle.go). - - `Probe`: indicate whether the plugin is ready to serve requests; this - example is stateless, so it will always be ready. +# 2. Continuous WAL streaming +pg_receivewal --slot wal_replica --compress 0 --directory /path/to/wal --dbname "postgres://..." [--synchronous] [--verbose] +``` -### Lifecycle +### Security Implementation -This example implements the lifecycle service capabilities to add labels and -annotations to the pods. The `OperatorLifecycleServer` interface is implemented -inside the `internal/lifecycle` package. +**TLS Configuration:** +- Uses cluster-managed certificates from CloudNativePG +- Mounts CA certificate for SSL verification +- Client certificate authentication for `streaming_replica` user +- `sslmode=verify-full` for maximum security -The `OperatorLifecycleServer` interface requires several methods: +**Pod Security:** +- Runs as PostgreSQL user (`uid: 105, gid: 103`) +- Proper filesystem permissions (`fsGroup: 103`) +- Read-only certificate mounts -- `GetCapabilities`: describe the resources and operations the plugin should be - notified for +## Development Environment Setup -- `LifecycleHook`: is invoked for every operation against the Kubernetes API - server that matches the specifications returned by `GetCapabilities` +### Prerequisites - In this function, the plugin is expected to do pattern matching using - the `Kind` and the operation `Type` and proceed with the proper logic. +```bash +# Required tools +go 1.24.1+ +docker or podman +kubectl +kind (for local testing) -### Operator +# Required Kubernetes components +cloudnative-pg operator +cnpg-i framework +cert-manager (for TLS) +``` -The operator interface offers a way for the plugin to interact with the Cluster -resource webhooks. +### Local Development -Do that, the plugin should implement -the [operator](https://github.com/cloudnative-pg/cnpg-i/blob/main/proto/operator.proto) -interface, specifically the `MutateCluster`, `ValidateClusterCreate`, -and `ValidateClusterChange` rpc calls. +#### 1. Environment Setup -- `MutateCluster`: enriches the plugin defaulting webhook +```bash +# Clone repository +git clone https://github.com/documentdb/cnpg-i-wal-replica +cd cnpg-i-wal-replica -- `ValidateClusterCreate` and `ValidateClusterChange`: enriches the plugin - validation logic. +# Install dependencies +go mod download -The package `internal/operator` implements this interface. +# Verify build +go build -o bin/cnpg-i-wal-replica main.go +``` -### Startup Command +#### 2. Code Structure Navigation -The plugin runs in its own pod, and its main command is implemented in -the `main.go` file. +``` +├── cmd/plugin/ # CLI interface and gRPC server setup +│ ├── doc.go # Package documentation +│ └── plugin.go # Main command and service registration +├── internal/ +│ ├── config/ # Configuration management +│ │ ├── config.go # Configuration types and validation +│ │ └── doc.go # Package documentation +│ ├── identity/ # Plugin identity implementation +│ │ ├── impl.go # Identity service methods +│ │ └── doc.go # Package documentation +│ ├── k8sclient/ # Kubernetes client utilities +│ │ ├── k8sclient.go # Client initialization and management +│ │ └── doc.go # Package documentation +│ ├── operator/ # Operator interface implementation +│ │ ├── impl.go # Core operator methods +│ │ ├── mutations.go # Cluster mutation logic +│ │ ├── status.go # Status reporting +│ │ ├── validation.go# Parameter validation +│ │ └── doc.go # Package documentation +│ └── reconciler/ # Resource reconciliation +│ ├── impl.go # Reconciler hook implementation +│ ├── replica.go # WAL receiver resource management +│ └── doc.go # Package documentation +├── kubernetes/ # Deployment manifests +└── pkg/metadata/ # Plugin metadata constants +``` -This function uses the plugin helper library to create a GRPC server and manage -TLS. +#### 3. Testing Locally -Plugin developers are expected to use the `pluginhelper.CreateMainCmd` -to implement the `main` function, passing an implemented `Identity` -struct. +```bash +# Build and test +./scripts/build.sh -Further implementations can be registered within the callback function. +# Run with debugging +./scripts/run.sh -In the example we propose, that's done for **operator** and for the -**lifecycle** services in [file](../cmd/plugin/plugin.go): +# Test configuration parsing +go test ./internal/config/ -``` proto -operator.RegisterOperatorServer(server, operatorImpl.Implementation{}) -lifecycle.RegisterOperatorLifecycleServer(server, lifecycleImpl.Implementation{}) +# Test reconciliation logic +go test ./internal/reconciler/ ``` -## Build and deploy the plugin +### Container Development + +#### Building Images + +```bash +# Local build +docker build -t wal-replica-plugin:dev . + +# Multi-arch build +docker buildx build --platform linux/amd64,linux/arm64 -t wal-replica-plugin:latest . +``` + +#### Kubernetes Testing + +```bash +# Load into kind cluster +kind load docker-image wal-replica-plugin:dev + +# Apply manifests +kubectl apply -f kubernetes/ + +# Deploy test cluster +kubectl apply -f doc/examples/cluster-example.yaml +``` + +## Extending the Plugin + +### Adding New Parameters + +1. **Define in Configuration**: +```go +// internal/config/config.go +const MyNewParam = "myNewParam" + +type Configuration struct { + // existing fields... + MyNewValue string +} +``` + +2. **Add Validation**: +```go +// internal/config/config.go +func ValidateParams(helper *common.Plugin) []*operator.ValidationError { + // existing validation... + + if raw, present := helper.Parameters[MyNewParam]; present { + // Add validation logic + } +} +``` + +3. **Update Reconciliation**: +```go +// internal/reconciler/replica.go +func CreateWalReplica(ctx context.Context, cluster *cnpgv1.Cluster) error { + // Use configuration.MyNewValue in resource creation +} +``` + +### Adding Resource Management + +```go +// Example: Adding a Service resource +func createWalReceiverService(ctx context.Context, cluster *cnpgv1.Cluster) error { + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-wal-receiver", cluster.Name), + Namespace: cluster.Namespace, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: cluster.APIVersion, + Kind: cluster.Kind, + Name: cluster.Name, + UID: cluster.UID, + }}, + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{"app": fmt.Sprintf("%s-wal-receiver", cluster.Name)}, + Ports: []corev1.ServicePort{{ + Port: 5432, + TargetPort: intstr.FromInt(5432), + }}, + }, + } + + return k8sclient.MustGet().Create(ctx, service) +} +``` + +## Debugging and Troubleshooting + +### Common Development Issues + +1. **gRPC Connection Problems**: +```bash +# Check plugin registration +kubectl logs -l app=cnpg-i-wal-replica + +# Verify TLS certificates +kubectl describe secret -ca-secret +``` + +2. **Resource Creation Failures**: +```bash +# Check reconciler logs +kubectl logs deployment/-wal-receiver + +# Verify owner references +kubectl get deployment -wal-receiver -o yaml +``` + +3. **Parameter Validation Errors**: +```bash +# Check cluster events +kubectl describe cluster + +# Review validation logs +kubectl logs -l app=cnpg-operator +``` + +### Testing Configurations + +```yaml +# Test with minimal parameters +spec: + plugins: + - name: cnpg-i-wal-replica.documentdb.io + # No parameters - should use all defaults + +# Test with full configuration +spec: + plugins: + - name: cnpg-i-wal-replica.documentdb.io + parameters: + image: "postgres:16" + replicationHost: "my-cluster-rw" + synchronous: "active" + walDirectory: "/custom/wal/path" + walPVCSize: "50Gi" +``` + +## CI/CD Integration + +### GitHub Actions Workflow + +The repository includes automated workflows for: + +- **Build Verification**: Compiles plugin for multiple architectures +- **Container Publishing**: Builds and pushes container images +- **Manifest Generation**: Creates deployment artifacts +- **Integration Testing**: Tests against live CloudNativePG clusters + +### Deployment Artifacts + +Generated manifests include: +- Plugin deployment with proper RBAC +- Certificate management for TLS +- Service definitions for plugin discovery +- Example cluster configurations + +## Contributing Guidelines + +### Code Standards + +- Follow Go conventions and `gofmt` formatting +- Add comprehensive unit tests for new functionality +- Document all public interfaces and complex logic +- Use structured logging with appropriate levels + +### Pull Request Process + +1. Fork and create feature branch +2. Implement changes with tests +3. Update documentation +4. Submit PR with detailed description +5. Address review feedback + +### Testing Requirements + +- Unit tests for all new configuration parameters +- Integration tests for resource reconciliation +- End-to-end testing with real CloudNativePG clusters +- Performance testing for WAL streaming scenarios -Users can test their own changes to the plugin by building a container image -running it inside a Kubernetes cluster with CloudNativePG and cert-manager -installed. +## Future Development Roadmap -### Local build +### Planned Enhancements -The repository provides a [`Taskfile`](https://taskfile.dev/) that contains -several helpful commands to test the plugin in -a [CNPG development environment](https://github.com/cloudnative-pg/cloudnative-pg/tree/main/contribute/e2e_testing_environment#the-local-kubernetes-cluster-for-testing). +- **Enhanced Monitoring**: Prometheus metrics for WAL streaming +- **Multi-Zone Support**: Cross-region WAL archival capabilities +- **Backup Integration**: Coordination with CloudNativePG backup strategies +- **Resource Optimization**: Configurable resource requests/limits +- **Advanced Filtering**: WAL file retention and cleanup policies +- **Replica Support**: Extension to replica clusters for cascading replication -By executing `task local-kind-deploy`, a container image containing the -executable of the repository will be built and loaded inside the kind cluster. +### API Stability -Having done that, the wal-replica plugin deployment will be applied. +- Current API is alpha-level with potential breaking changes +- Plugin interface follows CNPG-I versioning conventions +- Configuration parameters may evolve based on user feedback -### CI/CD build +## Resources -The repository provides a GitHub Actions workflow that, on pushes, builds a -container image and generates a manifest file that can be used to deploy the -plugin. The manifest is attached to the workflow run as an artifact, and can be -applied to the cluster. +- [CloudNativePG Documentation](https://cloudnative-pg.io/) +- [CNPG-I Framework](https://github.com/cloudnative-pg/cnpg-i) +- [PostgreSQL WAL Documentation](https://www.postgresql.org/docs/current/wal.html) +- [Plugin Examples Repository](https://github.com/cloudnative-pg/cnpg-i-examples) diff --git a/plugins/wal-replica/pkg/metadata/doc.go b/plugins/wal-replica/pkg/metadata/doc.go index cca11b21..1670754a 100644 --- a/plugins/wal-replica/pkg/metadata/doc.go +++ b/plugins/wal-replica/pkg/metadata/doc.go @@ -16,7 +16,7 @@ var Data = identity.GetPluginMetadataResponse{ DisplayName: "WAL Replica Pod Manager", ProjectUrl: "https://github.com/documentdb/cnpg-i-wal-replica", RepositoryUrl: "https://github.com/documentdb/cnpg-i-wal-replica", - License: "Proprietary", + License: "MIT", LicenseUrl: "https://github.com/documentdb/cnpg-i-wal-replica/LICENSE", Maturity: "alpha", } From e5ada08e96f345512c4c1738b8a779dcce296e81 Mon Sep 17 00:00:00 2001 From: Alexander Laye Date: Tue, 30 Sep 2025 11:58:05 -0400 Subject: [PATCH 10/12] remove plugin-side primary detection --- plugins/wal-replica/internal/reconciler/replica.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/plugins/wal-replica/internal/reconciler/replica.go b/plugins/wal-replica/internal/reconciler/replica.go index 7d11630b..a448f044 100644 --- a/plugins/wal-replica/internal/reconciler/replica.go +++ b/plugins/wal-replica/internal/reconciler/replica.go @@ -28,11 +28,6 @@ func CreateWalReplica( ) error { logger := log.FromContext(ctx).WithName("CreateWalReplica") - if !IsPrimaryCluster(cluster) { - logger.Info("Cluster is not a primary, skipping wal replica creation", "cluster", cluster.Name) - return nil - } - // Build Deployment name unique per cluster deploymentName := fmt.Sprintf("%s-wal-receiver", cluster.Name) namespace := cluster.Namespace @@ -240,8 +235,3 @@ func int64Ptr(i int64) *int64 { func int32Ptr(i int32) *int32 { return &i } - -func IsPrimaryCluster(cluster *cnpgv1.Cluster) bool { - // TODO implement - return true -} From 2fb1584356769b69fd57e1602bc3b2e7cc806b19 Mon Sep 17 00:00:00 2001 From: German Date: Sun, 8 Mar 2026 22:13:17 +0000 Subject: [PATCH 11/12] refactor: improve WAL replica CNPG-I plugin robustness and testing Improvements informed by CNPG-I best practices (SCaLE 23x talk): - Fix PVC size: use configurable walPVCSize instead of hard-coded 10Gi - Fix OwnerReferences: add Controller=true and BlockOwnerDeletion=true for proper garbage collection when clusters are deleted - Implement Deployment patch/update on configuration changes (was TODO) - Add liveness and readiness probes to the WAL receiver container - Add configurable parameters: verbose (bool), compression (0-9) - Align config.FromParameters to return validation errors (matches sidecar-injector pattern) - Add validation for new parameters (verbose, compression) - Document known CNPG-I limitations: MutateCluster not implemented upstream, SetStatusInCluster oscillation bug - Refactor reconciler into smaller testable functions - Add unit tests for config, identity, and reconciler packages (13 tests covering parsing, defaults, validation, roundtrip, command generation, deployment structure, owner references) - Update README with architecture diagram, CNPG-I interface table, known limitations section, and updated parameter reference Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/dockerfiles/Dockerfile_extension | 100 +++ .github/dockerfiles/Dockerfile_gateway_deb | 60 ++ .github/dockerfiles/gateway_entrypoint.sh | 68 ++ .../workflows/test-upgrade-and-rollback.yml | 676 ++++++++++++++++++ docs/crd-ref-docs-config.yaml | 24 + .../preview/api-reference.md | 423 +++++++++++ .../preview/high-availability/local-ha.md | 289 ++++++++ .../preview/high-availability/overview.md | 169 +++++ .../internal/config/config_test.go | 111 +++ plugins/wal-replica/README.md | 182 ++--- plugins/wal-replica/internal/config/config.go | 205 +++--- .../internal/config/config_test.go | 226 ++++++ .../internal/identity/impl_test.go | 68 ++ plugins/wal-replica/internal/operator/impl.go | 19 +- .../internal/operator/mutations.go | 14 +- .../wal-replica/internal/operator/status.go | 5 + .../internal/operator/validation.go | 4 +- .../internal/reconciler/replica.go | 486 +++++++------ .../internal/reconciler/replica_test.go | 176 +++++ 19 files changed, 2871 insertions(+), 434 deletions(-) create mode 100644 .github/dockerfiles/Dockerfile_extension create mode 100644 .github/dockerfiles/Dockerfile_gateway_deb create mode 100644 .github/dockerfiles/gateway_entrypoint.sh create mode 100644 .github/workflows/test-upgrade-and-rollback.yml create mode 100644 docs/crd-ref-docs-config.yaml create mode 100644 docs/operator-public-documentation/preview/api-reference.md create mode 100644 docs/operator-public-documentation/preview/high-availability/local-ha.md create mode 100644 docs/operator-public-documentation/preview/high-availability/overview.md create mode 100644 operator/cnpg-plugins/sidecar-injector/internal/config/config_test.go create mode 100644 plugins/wal-replica/internal/config/config_test.go create mode 100644 plugins/wal-replica/internal/identity/impl_test.go create mode 100644 plugins/wal-replica/internal/reconciler/replica_test.go diff --git a/.github/dockerfiles/Dockerfile_extension b/.github/dockerfiles/Dockerfile_extension new file mode 100644 index 00000000..c0317e50 --- /dev/null +++ b/.github/dockerfiles/Dockerfile_extension @@ -0,0 +1,100 @@ +# DocumentDB extension image for CNPG ImageVolume mode. +# Contains only extension artifacts (.so, .control, .sql, bitcode) and +# runtime shared-library dependencies. PostgreSQL is provided by the +# CNPG base image at runtime. +# +# Follows the pattern from: +# https://github.com/cloudnative-pg/postgres-extensions-containers +# +# Usage: +# docker build \ +# --build-arg PG_MAJOR=18 \ +# --build-arg DEB_PACKAGE_REL_PATH=packages/documentdb_0.110-0_arm64.deb \ +# -t documentdb-extension:latest \ +# -f Dockerfile_extension . + +ARG BASE=ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie +FROM ${BASE} AS builder + +ARG PG_MAJOR=18 +ARG DEB_PACKAGE_REL_PATH + +USER 0 + +RUN set -eux && \ + # Snapshot base image system libraries for later diffing + ldconfig -p | awk '{print $NF}' | grep '^/' | sort | uniq > /tmp/base-image-libs.out && \ + # Install pgdg extension packages + apt-get update && \ + apt-get install -y --no-install-recommends \ + postgresql-${PG_MAJOR}-cron \ + postgresql-${PG_MAJOR}-pgvector \ + postgresql-${PG_MAJOR}-postgis-3 + +# Install the DocumentDB extension from a pre-built .deb +COPY ${DEB_PACKAGE_REL_PATH} /tmp/documentdb.deb +RUN dpkg -i /tmp/documentdb.deb && \ + rm -f /tmp/documentdb.deb + +# Gather system library dependencies not present in the CNPG base image +RUN set -eux && \ + mkdir -p /system /licenses && \ + find /usr/lib/postgresql/${PG_MAJOR}/lib/ -name '*.so' -print0 | \ + xargs -0 ldd 2>/dev/null | \ + awk '{print $3}' | grep '^/' | sort | uniq > /tmp/all-deps.out && \ + comm -13 /tmp/base-image-libs.out /tmp/all-deps.out > /tmp/libraries.out && \ + while read -r lib; do \ + resolved=$(readlink -f "$lib"); \ + dir=$(dirname "$lib"); \ + base=$(basename "$lib"); \ + cp -a "$resolved" /system/; \ + for file in "$dir"/"${base%.so*}.so"*; do \ + [ -e "$file" ] || continue; \ + if [ -L "$file" ] && [ "$(readlink -f "$file")" = "$resolved" ]; then \ + ln -sf "$(basename "$resolved")" "/system/$(basename "$file")"; \ + fi; \ + done; \ + done < /tmp/libraries.out && \ + for lib in $(find /system -maxdepth 1 -type f -name '*.so*'); do \ + pkg=$(dpkg -S "$(basename "$lib")" 2>/dev/null | grep -v "diversion by" | awk -F: '/:/{print $1; exit}'); \ + [ -z "$pkg" ] && continue; \ + mkdir -p "/licenses/$pkg" && \ + cp -a "/usr/share/doc/$pkg/copyright" "/licenses/$pkg/copyright" 2>/dev/null || true; \ + done + +# Resolve Debian-alternatives symlinks — they break in ImageVolume mode +# because /etc/alternatives/ is outside the volume mount. +RUN set -eux && \ + for link in \ + /usr/share/postgresql/${PG_MAJOR}/extension/*.control \ + /usr/share/postgresql/${PG_MAJOR}/extension/*.sql \ + /usr/lib/postgresql/${PG_MAJOR}/lib/*.so; do \ + [ -L "$link" ] || continue; \ + target=$(readlink -f "$link" 2>/dev/null); \ + if [ -n "$target" ] && [ -f "$target" ]; then \ + cp --remove-destination "$target" "$link"; \ + elif [ -L "$link" ] && [ ! -e "$link" ]; then \ + rm -f "$link"; \ + fi; \ + done + +# ---------- minimal final image with only extension artifacts ---------- +FROM scratch +ARG PG_MAJOR=18 + +# Licenses +COPY --from=builder /licenses/ /licenses/ + +# Extension shared libraries (.so and bitcode) +COPY --from=builder /usr/lib/postgresql/${PG_MAJOR}/lib/ /lib/ + +# Extension SQL and control files +COPY --from=builder /usr/share/postgresql/${PG_MAJOR}/extension/ /share/extension/ + +# PostGIS supporting data (spatial_ref_sys, topology, etc.) +COPY --from=builder /usr/share/postgresql/${PG_MAJOR}/contrib/ /share/contrib/ + +# System library dependencies not present in the CNPG base image +COPY --from=builder /system/ /system/ + +USER 65532:65532 diff --git a/.github/dockerfiles/Dockerfile_gateway_deb b/.github/dockerfiles/Dockerfile_gateway_deb new file mode 100644 index 00000000..657be78f --- /dev/null +++ b/.github/dockerfiles/Dockerfile_gateway_deb @@ -0,0 +1,60 @@ +# Lean gateway image — contains only the DocumentDB gateway binary +# Designed for use with the DocumentDB Kubernetes Operator (CNPG-based) +# +# Usage: +# docker build \ +# --build-arg GATEWAY_DEB_REL_PATH=packages/documentdb_gateway_0.1.0-1_arm64.deb \ +# -t documentdb-gateway:latest \ +# -f Dockerfile_gateway_deb +# +# The build context must contain: +# - The gateway .deb at GATEWAY_DEB_REL_PATH +# - scripts/gateway_entrypoint.sh +# - scripts/utils.sh +# - pg_documentdb_gw/SetupConfiguration.json +ARG BASE_IMAGE=debian:trixie-slim +FROM ${BASE_IMAGE} + +ARG DEBIAN_FRONTEND=noninteractive +# Required: path (relative to build context) to the pre-built gateway .deb. +ARG GATEWAY_DEB_REL_PATH + +# Fail fast when the caller forgets the deb path. +RUN [ -n "${GATEWAY_DEB_REL_PATH}" ] || \ + (echo "ERROR: GATEWAY_DEB_REL_PATH build-arg is required." >&2; exit 1) + +# ---------- minimal runtime deps + PG client (psql, pg_isready) ---------- +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + jq \ + openssl \ + postgresql-client && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# ---------- gateway binary (from deb) ---------- +COPY ${GATEWAY_DEB_REL_PATH} /tmp/gateway.deb +RUN dpkg -i /tmp/gateway.deb && \ + rm -f /tmp/gateway.deb + +# ---------- runtime config & scripts ---------- +RUN useradd -ms /bin/bash -u 1000 documentdb + +RUN mkdir -p /home/documentdb/gateway/scripts \ + && mkdir -p /home/documentdb/gateway/pg_documentdb_gw/target + +COPY pg_documentdb_gw/SetupConfiguration.json \ + /home/documentdb/gateway/pg_documentdb_gw/SetupConfiguration.json +COPY scripts/utils.sh \ + /home/documentdb/gateway/scripts/utils.sh +COPY scripts/gateway_entrypoint.sh \ + /home/documentdb/gateway/scripts/gateway_entrypoint.sh + +RUN chown -R documentdb:documentdb /home/documentdb/gateway \ + && chmod +x /home/documentdb/gateway/scripts/*.sh + +USER documentdb +WORKDIR /home/documentdb/gateway + +ENTRYPOINT ["/bin/bash", "/home/documentdb/gateway/scripts/gateway_entrypoint.sh"] diff --git a/.github/dockerfiles/gateway_entrypoint.sh b/.github/dockerfiles/gateway_entrypoint.sh new file mode 100644 index 00000000..dedcdcab --- /dev/null +++ b/.github/dockerfiles/gateway_entrypoint.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# Lean gateway entrypoint for CNPG sidecar mode. +# Handles args passed by the sidecar-injector plugin: +# --create-user, --start-pg, --pg-port, --cert-path, --key-file +set -e + +CREATE_USER="true" +PG_PORT="5432" +CERT_PATH="" +KEY_FILE="" +# In CNPG clusters the superuser is always "postgres" +OWNER="${OWNER:-postgres}" +USERNAME="${USERNAME:-}" +PASSWORD="${PASSWORD:-}" + +while [[ $# -gt 0 ]]; do + case $1 in + --create-user) shift; CREATE_USER="$1"; shift;; + --start-pg) shift; shift;; # ignored — PG managed by CNPG + --pg-port) shift; PG_PORT="$1"; shift;; + --cert-path) shift; CERT_PATH="$1"; shift;; + --key-file) shift; KEY_FILE="$1"; shift;; + --owner) shift; OWNER="$1"; shift;; + --username) shift; USERNAME="$1"; shift;; + --password) shift; PASSWORD="$1"; shift;; + *) echo "Unknown option: $1" >&2; shift;; + esac +done + +# PG is in a separate container; force TCP connection via localhost +export PGHOST=localhost + +# Set up gateway configuration +CONFIG="/home/documentdb/gateway/pg_documentdb_gw/target/SetupConfiguration_temp.json" +cp /home/documentdb/gateway/pg_documentdb_gw/SetupConfiguration.json "$CONFIG" + +if ! [[ "$PG_PORT" =~ ^[0-9]+$ ]]; then + echo "ERROR: PG_PORT must be a number, got: $PG_PORT" >&2 + exit 1 +fi +jq --argjson port "$PG_PORT" '.PostgresPort = $port' "$CONFIG" > "$CONFIG.tmp" && mv "$CONFIG.tmp" "$CONFIG" + +if [ -n "$CERT_PATH" ] && [ -n "$KEY_FILE" ]; then + jq --arg c "$CERT_PATH" --arg k "$KEY_FILE" \ + '.CertificateOptions = {"CertType":"PemFile","FilePath":$c,"KeyFilePath":$k}' \ + "$CONFIG" > "$CONFIG.tmp" && mv "$CONFIG.tmp" "$CONFIG" +fi + +# Wait for PostgreSQL (TCP readiness check) +echo "Waiting for PostgreSQL on localhost:$PG_PORT..." +timeout=600; elapsed=0 +while ! pg_isready -h localhost -p "$PG_PORT" -q 2>/dev/null; do + if [ "$elapsed" -ge "$timeout" ]; then + echo "PostgreSQL did not become ready within ${timeout}s"; exit 1 + fi + sleep 2; elapsed=$((elapsed + 2)) +done +echo "PostgreSQL is ready." + +# Create admin user if requested +if [ "$CREATE_USER" = "true" ] && [ -n "$USERNAME" ] && [ -n "$PASSWORD" ]; then + echo "Creating admin user $USERNAME..." + source /home/documentdb/gateway/scripts/utils.sh + SetupCustomAdminUser "$USERNAME" "$PASSWORD" "$PG_PORT" "$OWNER" +fi + +# Start gateway +exec /usr/bin/documentdb_gateway "$CONFIG" diff --git a/.github/workflows/test-upgrade-and-rollback.yml b/.github/workflows/test-upgrade-and-rollback.yml new file mode 100644 index 00000000..d2f9d345 --- /dev/null +++ b/.github/workflows/test-upgrade-and-rollback.yml @@ -0,0 +1,676 @@ +name: TEST - Upgrade and Rollback + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + schedule: + - cron: '0 2 * * *' + workflow_dispatch: + inputs: + image_tag: + description: 'Optional: Use existing image tag instead of building locally' + required: false + type: string + workflow_call: + inputs: + image_tag: + description: 'Optional: Use existing image tag instead of building locally' + required: false + type: string + +permissions: + contents: read + actions: read + packages: read + +env: + CERT_MANAGER_NS: cert-manager + OPERATOR_NS: documentdb-operator + DB_NS: documentdb-upgrade-test + DB_NAME: documentdb-upgrade + DB_USERNAME: k8s_secret_user + DB_PASSWORD: K8sSecret100 + DB_PORT: 10260 + +jobs: + build: + name: Build Images and Charts + if: ${{ (inputs.image_tag == '' || inputs.image_tag == null) || github.event_name == 'pull_request' }} + uses: ./.github/workflows/test-build-and-package.yml + with: + version: '0.1.1' + secrets: inherit + + upgrade-and-rollback-test: + name: Upgrade & Rollback (${{ matrix.architecture }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + needs: build + if: always() && (needs.build.result == 'success' || needs.build.result == 'skipped') + + strategy: + matrix: + include: + - architecture: amd64 + runner: ubuntu-22.04 + test_scenario_name: "single-node" + node_count: 1 + instances_per_node: 1 + - architecture: arm64 + runner: ubuntu-22.04-arm + test_scenario_name: "single-node" + node_count: 1 + instances_per_node: 1 + + env: + IMAGE_TAG: ${{ (github.event_name == 'pull_request' || inputs.image_tag == '' || inputs.image_tag == null) && needs.build.outputs.image_tag || inputs.image_tag }} + CHART_VERSION: ${{ needs.build.outputs.chart_version || '0.1.0' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download artifacts + if: ${{ (inputs.image_tag == '' || inputs.image_tag == null) || github.event_name == 'pull_request' }} + uses: actions/download-artifact@v4 + with: + pattern: 'build-*' + path: ./artifacts + + - name: Determine new DocumentDB and Gateway image references + run: | + # Keep upgrade/rollback workflow self-contained by using images from this run. + if [[ "${{ github.event_name }}" == "pull_request" || -z "${{ inputs.image_tag }}" ]]; then + NEW_DOCDB="ghcr.io/${{ github.repository_owner }}/documentdb-kubernetes-operator/documentdb:${{ env.IMAGE_TAG }}-${{ matrix.architecture }}" + NEW_GW="ghcr.io/${{ github.repository_owner }}/documentdb-kubernetes-operator/gateway:${{ env.IMAGE_TAG }}-${{ matrix.architecture }}" + else + NEW_DOCDB="ghcr.io/${{ github.repository_owner }}/documentdb-kubernetes-operator/documentdb:${{ env.IMAGE_TAG }}" + NEW_GW="ghcr.io/${{ github.repository_owner }}/documentdb-kubernetes-operator/gateway:${{ env.IMAGE_TAG }}" + fi + OLD_DOCDB="$NEW_DOCDB" + OLD_GW="$NEW_GW" + echo "DOCUMENTDB_IMAGE=$NEW_DOCDB" >> $GITHUB_ENV + echo "GATEWAY_IMAGE=$NEW_GW" >> $GITHUB_ENV + echo "DOCUMENTDB_OLD_IMAGE=$OLD_DOCDB" >> $GITHUB_ENV + echo "GATEWAY_OLD_IMAGE=$OLD_GW" >> $GITHUB_ENV + echo "Old DocumentDB image: $OLD_DOCDB" + echo "Old Gateway image: $OLD_GW" + echo "New DocumentDB image: $NEW_DOCDB" + echo "New Gateway image: $NEW_GW" + + - name: Log test configuration + run: | + echo "## Upgrade & Rollback Test Configuration" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [[ -n "${{ inputs.image_tag }}" ]]; then + echo "- **Mode**: Using provided image tag" >> $GITHUB_STEP_SUMMARY + echo "- **Image Tag**: \`${{ inputs.image_tag }}\`" >> $GITHUB_STEP_SUMMARY + else + echo "- **Mode**: Using locally built images" >> $GITHUB_STEP_SUMMARY + echo "- **Image Tag**: \`${{ env.IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY + fi + echo "- **Architecture**: \`${{ matrix.architecture }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Old Extension Image**: \`${{ env.DOCUMENTDB_OLD_IMAGE }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **New Extension Image**: \`${{ env.DOCUMENTDB_IMAGE }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Old Gateway Image**: \`${{ env.GATEWAY_OLD_IMAGE }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **New Gateway Image**: \`${{ env.GATEWAY_IMAGE }}\`" >> $GITHUB_STEP_SUMMARY + + - name: Setup test environment + uses: ./.github/actions/setup-test-environment + with: + test-type: 'e2e' + architecture: ${{ matrix.architecture }} + runner: ${{ matrix.runner }} + test-scenario-name: ${{ matrix.test_scenario_name }} + node-count: '${{ matrix.node_count }}' + instances-per-node: '${{ matrix.instances_per_node }}' + cert-manager-namespace: ${{ env.CERT_MANAGER_NS }} + operator-namespace: ${{ env.OPERATOR_NS }} + db-namespace: ${{ env.DB_NS }} + db-cluster-name: ${{ env.DB_NAME }} + db-username: ${{ env.DB_USERNAME }} + db-password: ${{ env.DB_PASSWORD }} + db-port: ${{ env.DB_PORT }} + image-tag: ${{ env.IMAGE_TAG }} + chart-version: ${{ env.CHART_VERSION }} + documentdb-image: ${{ env.DOCUMENTDB_OLD_IMAGE }} + gateway-image: ${{ env.GATEWAY_OLD_IMAGE }} + use-external-images: ${{ github.event_name != 'pull_request' && inputs.image_tag != '' && inputs.image_tag != null }} + github-token: ${{ secrets.GITHUB_TOKEN }} + repository-owner: ${{ github.repository_owner }} + + - name: Setup port forwarding for data seeding + uses: ./.github/actions/setup-port-forwarding + with: + namespace: ${{ env.DB_NS }} + cluster-name: ${{ env.DB_NAME }} + port: ${{ env.DB_PORT }} + architecture: ${{ matrix.architecture }} + test-type: 'comprehensive' + + - name: Seed test data before upgrade + run: | + echo "=== Data Persistence: Writing seed data before upgrade ===" + mongosh 127.0.0.1:$DB_PORT \ + -u $DB_USERNAME \ + -p $DB_PASSWORD \ + --authenticationMechanism SCRAM-SHA-256 \ + --tls \ + --tlsAllowInvalidCertificates \ + --eval ' + db = db.getSiblingDB("upgrade_test_db"); + db.test_collection.insertOne({ _id: "upgrade_marker", step: "pre-upgrade", timestamp: new Date().toISOString() }); + db.test_collection.insertOne({ _id: "persistence_check", data: "this_must_survive_rollback", count: 42 }); + var count = db.test_collection.countDocuments(); + print("✓ Seed data written: " + count + " documents"); + assert(count === 2, "Expected 2 documents but found " + count); + ' + echo "✓ Seed data written successfully on old version" + + - name: Cleanup port forwarding after data seeding + if: always() + run: | + if [ -f /tmp/pf_pid ]; then + PF_PID=$(cat /tmp/pf_pid) + kill $PF_PID 2>/dev/null || true + rm -f /tmp/pf_pid + fi + rm -f /tmp/pf_output.log + + - name: "Step 1: Upgrade Both Extension and Gateway Images" + run: | + echo "=== Step 1: Upgrade Both Extension and Gateway Images ===" + echo "Testing simultaneous extension + gateway upgrade on ${{ matrix.architecture }}..." + + OLD_EXTENSION="${{ env.DOCUMENTDB_OLD_IMAGE }}" + NEW_EXTENSION="${{ env.DOCUMENTDB_IMAGE }}" + OLD_GATEWAY="${{ env.GATEWAY_OLD_IMAGE }}" + NEW_GATEWAY="${{ env.GATEWAY_IMAGE }}" + + # Verify baseline: cluster deployed with old images + CURRENT_EXTENSION=$(kubectl get documentdb $DB_NAME -n $DB_NS -o jsonpath='{.spec.documentDBImage}') + echo "Current extension image: $CURRENT_EXTENSION" + if [[ "$CURRENT_EXTENSION" != "$OLD_EXTENSION" ]]; then + echo "❌ Expected old extension image $OLD_EXTENSION but found $CURRENT_EXTENSION" + exit 1 + fi + + CURRENT_GATEWAY=$(kubectl get cluster $DB_NAME -n $DB_NS -o jsonpath='{.spec.plugins[0].parameters.gatewayImage}') + echo "Current gateway image: $CURRENT_GATEWAY" + if [[ "$CURRENT_GATEWAY" != "$OLD_GATEWAY" ]]; then + echo "❌ Expected old gateway image $OLD_GATEWAY but found $CURRENT_GATEWAY" + exit 1 + fi + echo "✓ Cluster deployed with old images" + + # Record and verify version before upgrade + VERSION_BEFORE=$(kubectl get documentdb $DB_NAME -n $DB_NS -o jsonpath='{.status.schemaVersion}') + echo "DocumentDB schema version before upgrade: $VERSION_BEFORE" + + if [[ -z "$VERSION_BEFORE" ]]; then + echo "❌ status.schemaVersion is empty before upgrade" + exit 1 + fi + echo "✓ DocumentDB schema version is populated before upgrade" + + # Patch both images simultaneously + echo "" + echo "Upgrading both images..." + echo " Extension: $OLD_EXTENSION → $NEW_EXTENSION" + echo " Gateway: $OLD_GATEWAY → $NEW_GATEWAY" + kubectl patch documentdb $DB_NAME -n $DB_NS --type='merge' \ + -p "{\"spec\":{\"documentDBImage\":\"$NEW_EXTENSION\",\"gatewayImage\":\"$NEW_GATEWAY\"}}" + + echo "Waiting for cluster to be healthy with new images..." + timeout 600 bash -c ' + while true; do + DB_STATUS=$(kubectl get documentdb '$DB_NAME' -n '$DB_NS' -o jsonpath="{.status.status}" 2>/dev/null) + CLUSTER_STATUS=$(kubectl get cluster '$DB_NAME' -n '$DB_NS' -o jsonpath="{.status.phase}" 2>/dev/null) + SCHEMA_VERSION=$(kubectl get documentdb '$DB_NAME' -n '$DB_NS' -o jsonpath="{.status.schemaVersion}" 2>/dev/null || echo "N/A") + echo "DocumentDB status: $DB_STATUS, CNPG phase: $CLUSTER_STATUS, schemaVersion: $SCHEMA_VERSION" + if [[ "$DB_STATUS" == "Cluster in healthy state" && "$CLUSTER_STATUS" == "Cluster in healthy state" ]]; then + HEALTHY_PODS=$(kubectl get cluster '$DB_NAME' -n '$DB_NS' -o jsonpath="{.status.instancesStatus.healthy}" 2>/dev/null | jq length 2>/dev/null || echo "0") + if [[ "$HEALTHY_PODS" -ge "1" ]]; then + # Verify pods are actually running the new extension image + # With ImageVolume (K8s >= 1.35), the extension image is mounted as a volume, not an init container + POD_IMAGES=$(kubectl get pods -n '$DB_NS' -l cnpg.io/cluster='$DB_NAME' -o jsonpath="{.items[*].spec.volumes[*].image.reference}" 2>/dev/null) + if echo "$POD_IMAGES" | grep -q "'"$NEW_EXTENSION"'"; then + echo "✓ Cluster healthy with $HEALTHY_PODS pods running new images" + break + else + echo "Pods not yet running new extension image, waiting..." + fi + fi + fi + sleep 10 + done + ' + + # Verify extension image + FINAL_EXTENSION=$(kubectl get documentdb $DB_NAME -n $DB_NS -o jsonpath='{.spec.documentDBImage}') + if [[ "$FINAL_EXTENSION" != "$NEW_EXTENSION" ]]; then + echo "❌ Extension image not applied: expected $NEW_EXTENSION, got $FINAL_EXTENSION" + exit 1 + fi + echo "✓ Extension image upgraded to $NEW_EXTENSION" + + # Verify gateway image in CNPG cluster + FINAL_GATEWAY=$(kubectl get cluster $DB_NAME -n $DB_NS -o jsonpath='{.spec.plugins[0].parameters.gatewayImage}') + if [[ "$FINAL_GATEWAY" != "$NEW_GATEWAY" ]]; then + echo "❌ Gateway image not applied: expected $NEW_GATEWAY, got $FINAL_GATEWAY" + exit 1 + fi + echo "✓ Gateway image upgraded to $NEW_GATEWAY" + + # Verify DocumentDB schema version updated + VERSION_AFTER=$(kubectl get documentdb $DB_NAME -n $DB_NS -o jsonpath='{.status.schemaVersion}') + echo "DocumentDB schema version after upgrade: $VERSION_AFTER" + if [[ -z "$VERSION_AFTER" ]]; then + echo "❌ status.schemaVersion is empty after upgrade" + exit 1 + fi + + # Verify status fields + echo "" + echo "=== Status Field Verification ===" + STATUS_DB_IMAGE=$(kubectl get documentdb $DB_NAME -n $DB_NS -o jsonpath='{.status.documentDBImage}') + echo "status.documentDBImage: $STATUS_DB_IMAGE" + if [[ "$STATUS_DB_IMAGE" == "$NEW_EXTENSION" ]]; then + echo "✓ status.documentDBImage matches new extension image" + else + echo "⚠️ status.documentDBImage ($STATUS_DB_IMAGE) does not match expected ($NEW_EXTENSION)" + fi + + STATUS_GATEWAY_IMAGE=$(kubectl get documentdb $DB_NAME -n $DB_NS -o jsonpath='{.status.gatewayImage}') + echo "status.gatewayImage: $STATUS_GATEWAY_IMAGE" + if [[ "$STATUS_GATEWAY_IMAGE" == "$NEW_GATEWAY" ]]; then + echo "✓ status.gatewayImage matches new gateway image" + else + echo "⚠️ status.gatewayImage ($STATUS_GATEWAY_IMAGE) does not match expected ($NEW_GATEWAY)" + fi + + echo "" + echo "✅ Step 1 passed: Both images upgraded successfully" + echo " Extension: $OLD_EXTENSION → $NEW_EXTENSION" + echo " Gateway: $OLD_GATEWAY → $NEW_GATEWAY" + + - name: Setup port forwarding for upgrade verification + uses: ./.github/actions/setup-port-forwarding + with: + namespace: ${{ env.DB_NS }} + cluster-name: ${{ env.DB_NAME }} + port: ${{ env.DB_PORT }} + architecture: ${{ matrix.architecture }} + test-type: 'comprehensive' + + - name: Verify data persistence after upgrade + run: | + echo "=== Data Persistence: Verifying after upgrade ===" + mongosh 127.0.0.1:$DB_PORT \ + -u $DB_USERNAME \ + -p $DB_PASSWORD \ + --authenticationMechanism SCRAM-SHA-256 \ + --tls \ + --tlsAllowInvalidCertificates \ + --eval ' + db = db.getSiblingDB("upgrade_test_db"); + var count = db.test_collection.countDocuments(); + assert(count === 2, "Expected 2 documents but found " + count + " after upgrade"); + print("✓ All " + count + " documents persisted through upgrade"); + ' + echo "✓ Data persistence verified after upgrade" + + - name: Cleanup port forwarding after upgrade verification + if: always() + run: | + if [ -f /tmp/pf_pid ]; then + PF_PID=$(cat /tmp/pf_pid) + kill $PF_PID 2>/dev/null || true + rm -f /tmp/pf_pid + fi + rm -f /tmp/pf_output.log + + - name: "Step 2: Rollback Extension Image (gateway stays at new version)" + run: | + echo "=== Step 2: Rollback Extension Image ===" + echo "Rolling back extension image while keeping gateway at new version..." + + OLD_EXTENSION="${{ env.DOCUMENTDB_OLD_IMAGE }}" + NEW_EXTENSION="${{ env.DOCUMENTDB_IMAGE }}" + NEW_GATEWAY="${{ env.GATEWAY_IMAGE }}" + + # Record state before rollback + VERSION_BEFORE=$(kubectl get documentdb $DB_NAME -n $DB_NS -o jsonpath='{.status.schemaVersion}') + echo "DocumentDB schema version before extension rollback: $VERSION_BEFORE" + + EVENTS_BEFORE=$(kubectl get events -n $DB_NS --field-selector reason=ExtensionRollback --no-headers 2>/dev/null | wc -l || echo "0") + echo "ExtensionRollback events before: $EVENTS_BEFORE" + + # Rollback only extension image + echo "" + echo "Patching spec.documentDBImage: $NEW_EXTENSION → $OLD_EXTENSION" + kubectl patch documentdb $DB_NAME -n $DB_NS --type='merge' \ + -p "{\"spec\":{\"documentDBImage\":\"$OLD_EXTENSION\"}}" + + echo "Waiting for cluster to stabilize after extension rollback..." + timeout 600 bash -c ' + while true; do + DB_STATUS=$(kubectl get documentdb '$DB_NAME' -n '$DB_NS' -o jsonpath="{.status.status}" 2>/dev/null) + CLUSTER_STATUS=$(kubectl get cluster '$DB_NAME' -n '$DB_NS' -o jsonpath="{.status.phase}" 2>/dev/null) + echo "DocumentDB status: $DB_STATUS, CNPG phase: $CLUSTER_STATUS" + if [[ "$DB_STATUS" == "Cluster in healthy state" && "$CLUSTER_STATUS" == "Cluster in healthy state" ]]; then + HEALTHY_PODS=$(kubectl get cluster '$DB_NAME' -n '$DB_NS' -o jsonpath="{.status.instancesStatus.healthy}" 2>/dev/null | jq length 2>/dev/null || echo "0") + if [[ "$HEALTHY_PODS" -ge "1" ]]; then + # Verify pods are running the rolled-back extension image + # With ImageVolume (K8s >= 1.35), the extension image is mounted as a volume, not an init container + POD_IMAGES=$(kubectl get pods -n '$DB_NS' -l cnpg.io/cluster='$DB_NAME' -o jsonpath="{.items[*].spec.volumes[*].image.reference}" 2>/dev/null) + if echo "$POD_IMAGES" | grep -q "'"$OLD_EXTENSION"'"; then + echo "✓ Cluster healthy with $HEALTHY_PODS pods running rolled-back extension image" + break + else + echo "Pods not yet running rolled-back extension image, waiting..." + fi + fi + fi + sleep 10 + done + ' + + echo "" + echo "=== Extension Rollback Verification ===" + + # Verify extension image rolled back + CURRENT_EXTENSION=$(kubectl get documentdb $DB_NAME -n $DB_NS -o jsonpath='{.spec.documentDBImage}') + if [[ "$CURRENT_EXTENSION" == "$OLD_EXTENSION" ]]; then + echo "✓ spec.documentDBImage correctly rolled back to $OLD_EXTENSION" + else + echo "❌ spec.documentDBImage should be $OLD_EXTENSION but is $CURRENT_EXTENSION" + exit 1 + fi + + # Verify schema version preserved (ALTER EXTENSION UPDATE skipped) + VERSION_AFTER=$(kubectl get documentdb $DB_NAME -n $DB_NS -o jsonpath='{.status.schemaVersion}') + echo "DocumentDB schema version before rollback: $VERSION_BEFORE" + echo "DocumentDB schema version after rollback: $VERSION_AFTER" + if [[ "$VERSION_AFTER" == "$VERSION_BEFORE" ]]; then + echo "✓ Schema version preserved — ALTER EXTENSION UPDATE correctly skipped" + else + echo "⚠️ Schema version changed from $VERSION_BEFORE to $VERSION_AFTER" + fi + + # Verify ExtensionRollback warning event (poll up to 60s instead of hardcoded sleep) + echo "Waiting for ExtensionRollback event..." + EVENTS_AFTER=$EVENTS_BEFORE + for i in $(seq 1 12); do + EVENTS_AFTER=$(kubectl get events -n $DB_NS --field-selector reason=ExtensionRollback --no-headers 2>/dev/null | wc -l || echo "0") + if [[ "$EVENTS_AFTER" -gt "$EVENTS_BEFORE" ]]; then + break + fi + sleep 5 + done + echo "ExtensionRollback events after: $EVENTS_AFTER" + if [[ "$EVENTS_AFTER" -gt "$EVENTS_BEFORE" ]]; then + echo "✓ ExtensionRollback warning event detected" + kubectl get events -n $DB_NS --field-selector reason=ExtensionRollback + else + echo "⚠️ No new ExtensionRollback event detected within 60s" + kubectl get events -n $DB_NS --sort-by='.lastTimestamp' | tail -20 + fi + + # Verify gateway image UNCHANGED at new version + CURRENT_GATEWAY=$(kubectl get cluster $DB_NAME -n $DB_NS -o jsonpath='{.spec.plugins[0].parameters.gatewayImage}') + echo "" + echo "Gateway image after extension rollback: $CURRENT_GATEWAY" + if [[ "$CURRENT_GATEWAY" == "$NEW_GATEWAY" ]]; then + echo "✓ Gateway image unchanged at $NEW_GATEWAY (extension rollback did not affect gateway)" + else + echo "❌ Gateway image changed unexpectedly: expected $NEW_GATEWAY, got $CURRENT_GATEWAY" + exit 1 + fi + + # Verify status fields + STATUS_DB_IMAGE=$(kubectl get documentdb $DB_NAME -n $DB_NS -o jsonpath='{.status.documentDBImage}') + echo "status.documentDBImage: $STATUS_DB_IMAGE" + if [[ "$STATUS_DB_IMAGE" == "$OLD_EXTENSION" ]]; then + echo "✓ status.documentDBImage reflects rolled-back extension" + else + echo "⚠️ status.documentDBImage ($STATUS_DB_IMAGE) does not match $OLD_EXTENSION" + fi + + STATUS_GATEWAY_IMAGE=$(kubectl get documentdb $DB_NAME -n $DB_NS -o jsonpath='{.status.gatewayImage}') + echo "status.gatewayImage: $STATUS_GATEWAY_IMAGE" + if [[ "$STATUS_GATEWAY_IMAGE" == "$NEW_GATEWAY" ]]; then + echo "✓ status.gatewayImage still at new gateway version" + else + echo "⚠️ status.gatewayImage ($STATUS_GATEWAY_IMAGE) does not match $NEW_GATEWAY" + fi + + echo "" + echo "✅ Step 2 passed: Extension rolled back, gateway unchanged" + echo " Extension: $NEW_EXTENSION → $OLD_EXTENSION (rolled back)" + echo " Gateway: $NEW_GATEWAY (unchanged)" + + - name: Setup port forwarding for extension rollback verification + uses: ./.github/actions/setup-port-forwarding + with: + namespace: ${{ env.DB_NS }} + cluster-name: ${{ env.DB_NAME }} + port: ${{ env.DB_PORT }} + architecture: ${{ matrix.architecture }} + test-type: 'comprehensive' + + - name: Verify data persistence after extension rollback + run: | + echo "=== Data Persistence: Verifying after extension rollback ===" + mongosh 127.0.0.1:$DB_PORT \ + -u $DB_USERNAME \ + -p $DB_PASSWORD \ + --authenticationMechanism SCRAM-SHA-256 \ + --tls \ + --tlsAllowInvalidCertificates \ + --eval ' + db = db.getSiblingDB("upgrade_test_db"); + var count = db.test_collection.countDocuments(); + assert(count === 2, "Expected 2 documents but found " + count + " after extension rollback"); + print("✓ All " + count + " documents persisted through extension rollback"); + ' + echo "✓ Data persistence verified after extension rollback" + + - name: Cleanup port forwarding after extension rollback verification + if: always() + run: | + if [ -f /tmp/pf_pid ]; then + PF_PID=$(cat /tmp/pf_pid) + kill $PF_PID 2>/dev/null || true + rm -f /tmp/pf_pid + fi + rm -f /tmp/pf_output.log + + - name: "Step 3: Rollback Gateway Image (extension stays at old version)" + run: | + echo "=== Step 3: Rollback Gateway Image ===" + echo "Rolling back gateway image while keeping extension at old version..." + + OLD_EXTENSION="${{ env.DOCUMENTDB_OLD_IMAGE }}" + OLD_GATEWAY="${{ env.GATEWAY_OLD_IMAGE }}" + NEW_GATEWAY="${{ env.GATEWAY_IMAGE }}" + + # Record state before gateway rollback + VERSION_BEFORE=$(kubectl get documentdb $DB_NAME -n $DB_NS -o jsonpath='{.status.schemaVersion}') + echo "DocumentDB schema version before gateway rollback: $VERSION_BEFORE" + + # Rollback only gateway image + echo "" + echo "Patching spec.gatewayImage: $NEW_GATEWAY → $OLD_GATEWAY" + kubectl patch documentdb $DB_NAME -n $DB_NS --type='merge' \ + -p "{\"spec\":{\"gatewayImage\":\"$OLD_GATEWAY\"}}" + + echo "Waiting for cluster to stabilize after gateway rollback..." + timeout 600 bash -c ' + while true; do + DB_STATUS=$(kubectl get documentdb '$DB_NAME' -n '$DB_NS' -o jsonpath="{.status.status}" 2>/dev/null) + CLUSTER_STATUS=$(kubectl get cluster '$DB_NAME' -n '$DB_NS' -o jsonpath="{.status.phase}" 2>/dev/null) + echo "DocumentDB status: $DB_STATUS, CNPG phase: $CLUSTER_STATUS" + if [[ "$DB_STATUS" == "Cluster in healthy state" && "$CLUSTER_STATUS" == "Cluster in healthy state" ]]; then + HEALTHY_PODS=$(kubectl get cluster '$DB_NAME' -n '$DB_NS' -o jsonpath="{.status.instancesStatus.healthy}" 2>/dev/null | jq length 2>/dev/null || echo "0") + if [[ "$HEALTHY_PODS" -ge "1" ]]; then + # Verify gateway plugin parameter reflects the rolled-back image + CURRENT_GW_PARAM=$(kubectl get cluster '$DB_NAME' -n '$DB_NS' -o jsonpath="{.spec.plugins[0].parameters.gatewayImage}" 2>/dev/null) + if [[ "$CURRENT_GW_PARAM" == "'"$OLD_GATEWAY"'" ]]; then + echo "✓ Cluster healthy with $HEALTHY_PODS pods and gateway image rolled back" + break + else + echo "Gateway image not yet rolled back in cluster spec, waiting..." + fi + fi + fi + sleep 10 + done + ' + + echo "" + echo "=== Gateway Rollback Verification ===" + + # Verify gateway image rolled back in CNPG cluster + CURRENT_GATEWAY=$(kubectl get cluster $DB_NAME -n $DB_NS -o jsonpath='{.spec.plugins[0].parameters.gatewayImage}') + if [[ "$CURRENT_GATEWAY" == "$OLD_GATEWAY" ]]; then + echo "✓ Gateway image rolled back to $OLD_GATEWAY" + else + echo "❌ Gateway image should be $OLD_GATEWAY but is $CURRENT_GATEWAY" + exit 1 + fi + + # Verify extension image UNCHANGED at old version + CURRENT_EXTENSION=$(kubectl get documentdb $DB_NAME -n $DB_NS -o jsonpath='{.spec.documentDBImage}') + echo "Extension image after gateway rollback: $CURRENT_EXTENSION" + if [[ "$CURRENT_EXTENSION" == "$OLD_EXTENSION" ]]; then + echo "✓ Extension image unchanged at $OLD_EXTENSION (gateway rollback did not affect extension)" + else + echo "❌ Extension image changed unexpectedly: expected $OLD_EXTENSION, got $CURRENT_EXTENSION" + exit 1 + fi + + # Verify schema version unchanged (gateway is stateless, no schema impact) + VERSION_AFTER=$(kubectl get documentdb $DB_NAME -n $DB_NS -o jsonpath='{.status.schemaVersion}') + echo "DocumentDB schema version before gateway rollback: $VERSION_BEFORE" + echo "DocumentDB schema version after gateway rollback: $VERSION_AFTER" + if [[ "$VERSION_AFTER" == "$VERSION_BEFORE" ]]; then + echo "✓ Schema version unchanged — gateway rollback has no schema impact" + else + echo "⚠️ Schema version changed unexpectedly from $VERSION_BEFORE to $VERSION_AFTER" + fi + + # Verify status fields + STATUS_DB_IMAGE=$(kubectl get documentdb $DB_NAME -n $DB_NS -o jsonpath='{.status.documentDBImage}') + echo "status.documentDBImage: $STATUS_DB_IMAGE" + if [[ "$STATUS_DB_IMAGE" == "$OLD_EXTENSION" ]]; then + echo "✓ status.documentDBImage still at old extension" + else + echo "⚠️ status.documentDBImage ($STATUS_DB_IMAGE) does not match $OLD_EXTENSION" + fi + + STATUS_GATEWAY_IMAGE=$(kubectl get documentdb $DB_NAME -n $DB_NS -o jsonpath='{.status.gatewayImage}') + echo "status.gatewayImage: $STATUS_GATEWAY_IMAGE" + if [[ "$STATUS_GATEWAY_IMAGE" == "$OLD_GATEWAY" ]]; then + echo "✓ status.gatewayImage reflects rolled-back gateway" + else + echo "⚠️ status.gatewayImage ($STATUS_GATEWAY_IMAGE) does not match $OLD_GATEWAY" + fi + + echo "" + echo "✅ Step 3 passed: Gateway rolled back, extension unchanged" + echo " Extension: $OLD_EXTENSION (unchanged)" + echo " Gateway: $NEW_GATEWAY → $OLD_GATEWAY (rolled back)" + + - name: Setup port forwarding for gateway rollback verification + uses: ./.github/actions/setup-port-forwarding + with: + namespace: ${{ env.DB_NS }} + cluster-name: ${{ env.DB_NAME }} + port: ${{ env.DB_PORT }} + architecture: ${{ matrix.architecture }} + test-type: 'comprehensive' + + - name: Verify data persistence after gateway rollback + run: | + echo "=== Data Persistence: Verifying after gateway rollback ===" + mongosh 127.0.0.1:$DB_PORT \ + -u $DB_USERNAME \ + -p $DB_PASSWORD \ + --authenticationMechanism SCRAM-SHA-256 \ + --tls \ + --tlsAllowInvalidCertificates \ + --eval ' + db = db.getSiblingDB("upgrade_test_db"); + var count = db.test_collection.countDocuments(); + assert(count === 2, "Expected 2 documents but found " + count + " after gateway rollback"); + print("✓ All " + count + " documents persisted through full upgrade/rollback cycle"); + ' + echo "✓ Data persistence verified after gateway rollback" + + - name: Cleanup port forwarding after gateway rollback verification + if: always() + run: | + if [ -f /tmp/pf_pid ]; then + PF_PID=$(cat /tmp/pf_pid) + kill $PF_PID 2>/dev/null || true + rm -f /tmp/pf_pid + fi + rm -f /tmp/pf_output.log + + - name: Collect comprehensive logs on failure + if: failure() + uses: ./.github/actions/collect-logs + with: + architecture: ${{ matrix.architecture }} + operator-namespace: ${{ env.OPERATOR_NS }} + db-namespace: ${{ env.DB_NS }} + db-name: ${{ env.DB_NAME }} + + - name: Test completion summary + if: always() + run: | + echo "## Upgrade & Rollback Test Summary for ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Architecture**: ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY + echo "- **Old Extension Image**: ${{ env.DOCUMENTDB_OLD_IMAGE }}" >> $GITHUB_STEP_SUMMARY + echo "- **New Extension Image**: ${{ env.DOCUMENTDB_IMAGE }}" >> $GITHUB_STEP_SUMMARY + echo "- **Old Gateway Image**: ${{ env.GATEWAY_OLD_IMAGE }}" >> $GITHUB_STEP_SUMMARY + echo "- **New Gateway Image**: ${{ env.GATEWAY_IMAGE }}" >> $GITHUB_STEP_SUMMARY + echo "- **Image Tag**: ${{ env.IMAGE_TAG }}" >> $GITHUB_STEP_SUMMARY + echo "- **Chart Version**: ${{ env.CHART_VERSION }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ job.status }}" == "success" ]]; then + echo "- **Status**: ✅ PASSED" >> $GITHUB_STEP_SUMMARY + else + echo "- **Status**: ❌ FAILED" >> $GITHUB_STEP_SUMMARY + fi + + test-summary: + name: Upgrade & Rollback Test Summary + runs-on: ubuntu-latest + if: always() + needs: [build, upgrade-and-rollback-test] + steps: + - name: Generate overall test summary + run: | + echo "## Upgrade & Rollback Test Results Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Test Configuration:" >> $GITHUB_STEP_SUMMARY + echo "- **Build Step**: ${{ inputs.image_tag && 'Skipped (using external images)' || 'Executed' }}" >> $GITHUB_STEP_SUMMARY + echo "- **Image Tag**: ${{ inputs.image_tag || 'Built from source' }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Job Results:" >> $GITHUB_STEP_SUMMARY + echo "- **Build**: ${{ needs.build.result }}" >> $GITHUB_STEP_SUMMARY + echo "- **Upgrade & Rollback Tests**: ${{ needs.upgrade-and-rollback-test.result }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ needs.upgrade-and-rollback-test.result }}" == "success" ]]; then + echo "### Overall Status: ✅ ALL TESTS PASSED" >> $GITHUB_STEP_SUMMARY + else + echo "### Overall Status: ❌ SOME TESTS FAILED" >> $GITHUB_STEP_SUMMARY + echo "Check individual job results above for details." >> $GITHUB_STEP_SUMMARY + fi diff --git a/docs/crd-ref-docs-config.yaml b/docs/crd-ref-docs-config.yaml new file mode 100644 index 00000000..6e5f1139 --- /dev/null +++ b/docs/crd-ref-docs-config.yaml @@ -0,0 +1,24 @@ +processor: + # Exclude list types and status types from the generated documentation. + # Status types (TLSStatus, DocumentDBStatus) are read-only runtime state set by the operator. + # Since the status field is excluded via ignoreFields, excluding the status types + # prevents broken cross-references (e.g., TLSStatus "Appears in: DocumentDBStatus"). + ignoreTypes: + - "List$" + - "Status$" + # Exclude status and TypeMeta fields + ignoreFields: + - "status$" + - "TypeMeta$" + +render: + # Kubernetes version for linking to K8s API docs + kubernetesVersion: 1.35 + # Link external types to their upstream documentation + knownTypes: + - name: LocalObjectReference + package: github.com/cloudnative-pg/cloudnative-pg/api/v1 + link: https://pkg.go.dev/github.com/cloudnative-pg/cloudnative-pg/api/v1#LocalObjectReference + - name: AffinityConfiguration + package: github.com/cloudnative-pg/cloudnative-pg/api/v1 + link: https://pkg.go.dev/github.com/cloudnative-pg/cloudnative-pg/api/v1#AffinityConfiguration diff --git a/docs/operator-public-documentation/preview/api-reference.md b/docs/operator-public-documentation/preview/api-reference.md new file mode 100644 index 00000000..73f5144e --- /dev/null +++ b/docs/operator-public-documentation/preview/api-reference.md @@ -0,0 +1,423 @@ +# API Reference + +## Packages +- [documentdb.io/preview](#documentdbiopreview) + + +## documentdb.io/preview + +Package preview contains API Schema definitions for the db preview API group. + +### Resource Types +- [Backup](#backup) +- [DocumentDB](#documentdb) +- [ScheduledBackup](#scheduledbackup) + + + +#### Backup + + + + + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `documentdb.io/preview` | | | +| `kind` _string_ | `Backup` | | | +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | +| `spec` _[BackupSpec](#backupspec)_ | | | | + + +#### BackupConfiguration + + + +BackupConfiguration defines backup settings for DocumentDB. + + + +_Appears in:_ +- [DocumentDBSpec](#documentdbspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `retentionDays` _integer_ | RetentionDays specifies how many days backups should be retained.
If not specified, the default retention period is 30 days. | 30 | Maximum: 365
Minimum: 1
Optional: \{\}
| + + +#### BackupSpec + + + +BackupSpec defines the desired state of Backup. + + + +_Appears in:_ +- [Backup](#backup) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `cluster` _[LocalObjectReference](https://pkg.go.dev/github.com/cloudnative-pg/cloudnative-pg/api/v1#LocalObjectReference)_ | Cluster specifies the DocumentDB cluster to backup.
The cluster must exist in the same namespace as the Backup resource. | | Required: \{\}
| +| `retentionDays` _integer_ | RetentionDays specifies how many days the backup should be retained.
If not specified, the default retention period from the cluster's backup retention policy will be used. | | Optional: \{\}
| + + +#### BootstrapConfiguration + + + +BootstrapConfiguration defines how to bootstrap a DocumentDB cluster. + + + +_Appears in:_ +- [DocumentDBSpec](#documentdbspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `recovery` _[RecoveryConfiguration](#recoveryconfiguration)_ | Recovery configures recovery from a backup. | | Optional: \{\}
| + + +#### CertManagerTLS + + + +CertManagerTLS holds parameters for cert-manager driven certificates. + + + +_Appears in:_ +- [GatewayTLS](#gatewaytls) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `issuerRef` _[IssuerRef](#issuerref)_ | | | | +| `dnsNames` _string array_ | DNSNames for the certificate SANs. If empty, operator will add Service DNS names. | | | +| `secretName` _string_ | SecretName optional explicit name for the target secret. If empty a default is chosen. | | | + + +#### ClusterReplication + + + + + + + +_Appears in:_ +- [DocumentDBSpec](#documentdbspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `crossCloudNetworkingStrategy` _string_ | CrossCloudNetworking determines which type of networking mechanics for the replication | | Enum: [AzureFleet Istio None]
| +| `primary` _string_ | Primary is the name of the primary cluster for replication. | | | +| `clusterList` _[MemberCluster](#membercluster) array_ | ClusterList is the list of clusters participating in replication. | | | +| `highAvailability` _boolean_ | Whether or not to have replicas on the primary cluster. | | | + + +#### DocumentDB + + + +DocumentDB is the Schema for the dbs API. + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `documentdb.io/preview` | | | +| `kind` _string_ | `DocumentDB` | | | +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | +| `spec` _[DocumentDBSpec](#documentdbspec)_ | | | | + + +#### DocumentDBSpec + + + +DocumentDBSpec defines the desired state of DocumentDB. + + + +_Appears in:_ +- [DocumentDB](#documentdb) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `nodeCount` _integer_ | NodeCount is the number of nodes in the DocumentDB cluster. Must be 1. | | Maximum: 1
Minimum: 1
| +| `instancesPerNode` _integer_ | InstancesPerNode is the number of DocumentDB instances per node. Range: 1-3. | | Maximum: 3
Minimum: 1
| +| `resource` _[Resource](#resource)_ | Resource specifies the storage resources for DocumentDB. | | | +| `documentDBVersion` _string_ | DocumentDBVersion specifies the version for all DocumentDB components (engine, gateway).
When set, this overrides the default versions for documentDBImage and gatewayImage.
Individual image fields take precedence over this version. | | | +| `documentDBImage` _string_ | DocumentDBImage is the container image to use for DocumentDB.
Changing this is not recommended for most users.
If not specified, defaults based on documentDBVersion or operator defaults. | | | +| `gatewayImage` _string_ | GatewayImage is the container image to use for the DocumentDB Gateway sidecar.
Changing this is not recommended for most users.
If not specified, defaults to a version that matches the DocumentDB operator version. | | | +| `postgresImage` _string_ | PostgresImage is the container image to use for the PostgreSQL server.
If not specified, defaults to the last stable PostgreSQL version compatible with DocumentDB.
Must use trixie (Debian 13) base to match the extension's GLIBC requirements. | ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie | Optional: \{\}
| +| `documentDbCredentialSecret` _string_ | DocumentDbCredentialSecret is the name of the Kubernetes Secret containing credentials
for the DocumentDB gateway (expects keys `username` and `password`). If omitted,
a default secret name `documentdb-credentials` is used. | | | +| `clusterReplication` _[ClusterReplication](#clusterreplication)_ | ClusterReplication configures cross-cluster replication for DocumentDB. | | | +| `sidecarInjectorPluginName` _string_ | SidecarInjectorPluginName is the name of the sidecar injector plugin to use. | | | +| `walReplicaPluginName` _string_ | WalReplicaPluginName is the name of the wal replica plugin to use. | | | +| `exposeViaService` _[ExposeViaService](#exposeviaservice)_ | ExposeViaService configures how to expose DocumentDB via a Kubernetes service.
This can be a LoadBalancer or ClusterIP service. | | | +| `environment` _string_ | Environment specifies the cloud environment for deployment
This determines cloud-specific service annotations for LoadBalancer services | | Enum: [eks aks gke]
| +| `timeouts` _[Timeouts](#timeouts)_ | | | | +| `tls` _[TLSConfiguration](#tlsconfiguration)_ | TLS configures certificate management for DocumentDB components. | | | +| `logLevel` _string_ | Overrides default log level for the DocumentDB cluster. | | | +| `bootstrap` _[BootstrapConfiguration](#bootstrapconfiguration)_ | Bootstrap configures the initialization of the DocumentDB cluster. | | Optional: \{\}
| +| `backup` _[BackupConfiguration](#backupconfiguration)_ | Backup configures backup settings for DocumentDB. | | Optional: \{\}
| +| `featureGates` _object (keys:string, values:boolean)_ | FeatureGates enables or disables optional DocumentDB features.
Keys are PascalCase feature names following the Kubernetes feature gate convention.
Example: \{"ChangeStreams": true\}
IMPORTANT: When adding a new feature gate, update ALL of the following:
1. Add a new FeatureGate* constant in documentdb_types.go
2. Add the key name to the XValidation CEL rule's allowed list below
3. Add a default entry in the featureGateDefaults map in documentdb_types.go | | Optional: \{\}
| +| `affinity` _[AffinityConfiguration](https://pkg.go.dev/github.com/cloudnative-pg/cloudnative-pg/api/v1#AffinityConfiguration)_ | Affinity/Anti-affinity rules for Pods (cnpg passthrough) | | Optional: \{\}
| + + +#### ExposeViaService + + + + + + + +_Appears in:_ +- [DocumentDBSpec](#documentdbspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `serviceType` _string_ | ServiceType determines the type of service to expose for DocumentDB. | | Enum: [LoadBalancer ClusterIP]
| + + +#### GatewayTLS + + + +GatewayTLS defines TLS configuration for the gateway sidecar (Phase 1: certificate provisioning only) + + + +_Appears in:_ +- [TLSConfiguration](#tlsconfiguration) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `mode` _string_ | Mode selects the TLS management strategy. | | Enum: [Disabled SelfSigned CertManager Provided]
| +| `certManager` _[CertManagerTLS](#certmanagertls)_ | CertManager config when Mode=CertManager. | | | +| `provided` _[ProvidedTLS](#providedtls)_ | Provided secret reference when Mode=Provided. | | | + + +#### GlobalEndpointsTLS + + + +GlobalEndpointsTLS acts as a placeholder for future global endpoint TLS settings. + + + +_Appears in:_ +- [TLSConfiguration](#tlsconfiguration) + + + +#### IssuerRef + + + +IssuerRef references a cert-manager Issuer or ClusterIssuer. + + + +_Appears in:_ +- [CertManagerTLS](#certmanagertls) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `name` _string_ | | | | +| `kind` _string_ | Kind of issuer (Issuer or ClusterIssuer). Defaults to Issuer. | | | +| `group` _string_ | Group defaults to cert-manager.io | | | + + +#### MemberCluster + + + + + + + +_Appears in:_ +- [ClusterReplication](#clusterreplication) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `name` _string_ | Name is the name of the member cluster. | | | +| `environment` _string_ | EnvironmentOverride is the cloud environment of the member cluster.
Will default to the global setting | | Enum: [eks aks gke]
| +| `storageClass` _string_ | StorageClassOverride specifies the storage class for DocumentDB persistent volumes in this member cluster. | | | + + +#### PVRecoveryConfiguration + + + +PVRecoveryConfiguration defines settings for recovering from a retained PersistentVolume. + + + +_Appears in:_ +- [RecoveryConfiguration](#recoveryconfiguration) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `name` _string_ | Name is the name of the PersistentVolume to recover from.
The PV must exist and be in Available or Released state. | | MinLength: 1
| + + +#### PostgresTLS + + + +PostgresTLS acts as a placeholder for future Postgres TLS settings. + + + +_Appears in:_ +- [TLSConfiguration](#tlsconfiguration) + + + +#### ProvidedTLS + + + +ProvidedTLS references an existing secret that contains tls.crt/tls.key (and optional ca.crt). + + + +_Appears in:_ +- [GatewayTLS](#gatewaytls) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `secretName` _string_ | | | | + + +#### RecoveryConfiguration + + + +RecoveryConfiguration defines recovery settings for bootstrapping a DocumentDB cluster. + + + +_Appears in:_ +- [BootstrapConfiguration](#bootstrapconfiguration) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `backup` _[LocalObjectReference](https://pkg.go.dev/github.com/cloudnative-pg/cloudnative-pg/api/v1#LocalObjectReference)_ | Backup specifies the source backup to restore from. | | Optional: \{\}
| +| `persistentVolume` _[PVRecoveryConfiguration](#pvrecoveryconfiguration)_ | PersistentVolume specifies the PV to restore from.
The operator will create a temporary PVC bound to this PV, use it for CNPG recovery,
and delete the temporary PVC after the cluster is healthy.
Cannot be used together with Backup. | | Optional: \{\}
| + + +#### Resource + + + + + + + +_Appears in:_ +- [DocumentDBSpec](#documentdbspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `storage` _[StorageConfiguration](#storageconfiguration)_ | Storage configuration for DocumentDB persistent volumes. | | | + + +#### ScheduledBackup + + + + + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `documentdb.io/preview` | | | +| `kind` _string_ | `ScheduledBackup` | | | +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | +| `spec` _[ScheduledBackupSpec](#scheduledbackupspec)_ | | | | + + +#### ScheduledBackupSpec + + + +ScheduledBackupSpec defines the desired state of ScheduledBackup + + + +_Appears in:_ +- [ScheduledBackup](#scheduledbackup) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `cluster` _[LocalObjectReference](https://pkg.go.dev/github.com/cloudnative-pg/cloudnative-pg/api/v1#LocalObjectReference)_ | Cluster specifies the DocumentDB cluster to backup.
The cluster must exist in the same namespace as the ScheduledBackup resource. | | Required: \{\}
| +| `schedule` _string_ | Schedule defines when backups should be created using cron expression format.
See https://pkg.go.dev/github.com/robfig/cron#hdr-CRON_Expression_Format | | Required: \{\}
| +| `retentionDays` _integer_ | RetentionDays specifies how many days the backups should be retained.
If not specified, the default retention period from the cluster's backup retention policy will be used. | | Optional: \{\}
| + + +#### StorageConfiguration + + + + + + + +_Appears in:_ +- [Resource](#resource) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `pvcSize` _string_ | PvcSize is the size of the persistent volume claim for DocumentDB storage (e.g., "10Gi"). | | | +| `storageClass` _string_ | StorageClass specifies the storage class for DocumentDB persistent volumes.
If not specified, the cluster's default storage class will be used. | | | +| `persistentVolumeReclaimPolicy` _string_ | PersistentVolumeReclaimPolicy controls what happens to the PersistentVolume when
the DocumentDB cluster is deleted.
When a DocumentDB cluster is deleted, the following chain of deletions occurs:
DocumentDB deletion → CNPG Cluster deletion → PVC deletion → PV deletion (based on this policy)
Options:
- Retain (default): The PV is preserved after cluster deletion, allowing manual
data recovery or forensic analysis. Use for production workloads where data
safety is critical. Orphaned PVs must be manually deleted when no longer needed.
- Delete: The PV is automatically deleted when the PVC is deleted. Use for development,
testing, or ephemeral environments where data persistence is not required.
WARNING: Setting this to "Delete" means all data will be permanently lost when
the DocumentDB cluster is deleted. This cannot be undone. | Retain | Enum: [Retain Delete]
Optional: \{\}
| + + +#### TLSConfiguration + + + +TLSConfiguration aggregates TLS settings across DocumentDB components. + + + +_Appears in:_ +- [DocumentDBSpec](#documentdbspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `gateway` _[GatewayTLS](#gatewaytls)_ | Gateway configures TLS for the gateway sidecar (Phase 1: certificate provisioning only). | | | +| `postgres` _[PostgresTLS](#postgrestls)_ | Postgres configures TLS for the Postgres server (placeholder for future phases). | | | +| `globalEndpoints` _[GlobalEndpointsTLS](#globalendpointstls)_ | GlobalEndpoints configures TLS for global endpoints (placeholder for future phases). | | | + + +#### Timeouts + + + + + + + +_Appears in:_ +- [DocumentDBSpec](#documentdbspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `stopDelay` _integer_ | | | Maximum: 1800
Minimum: 0
| + + diff --git a/docs/operator-public-documentation/preview/high-availability/local-ha.md b/docs/operator-public-documentation/preview/high-availability/local-ha.md new file mode 100644 index 00000000..dfee5f01 --- /dev/null +++ b/docs/operator-public-documentation/preview/high-availability/local-ha.md @@ -0,0 +1,289 @@ +--- +title: Local High Availability +description: Configure local high availability for DocumentDB with multiple instances, pod anti-affinity, and automatic failover. +tags: + - high-availability + - configuration + - failover +--- + +# Local High Availability + +Local high availability (HA) deploys multiple DocumentDB instances within a single Kubernetes cluster, providing automatic failover and zero data loss during instance failures. + +## Overview + +Local HA uses synchronous replication between a primary instance and one or two replicas. When the primary fails, a replica is automatically promoted to primary. + +```mermaid +flowchart LR + subgraph zone1[Zone A] + P[Primary] + end + subgraph zone2[Zone B] + R1[Replica 1] + end + subgraph zone3[Zone C] + R2[Replica 2] + end + + App([Application]) --> P + P -->|Sync Replication| R1 + P -->|Sync Replication| R2 +``` + +## Instance Configuration + +Configure the number of instances using the `instancesPerNode` field: + +```yaml title="documentdb-ha.yaml" +apiVersion: documentdb.io/preview +kind: DocumentDB +metadata: + name: my-documentdb + namespace: documentdb +spec: + instancesPerNode: 3 # (1)! + storage: + size: 10Gi + storageClassName: managed-csi +``` + +1. Valid values: `1` (no HA), `2` (primary + 1 replica), `3` (primary + 2 replicas, recommended for production) + +### Instance Count Options + +| Instances | Configuration | Use Case | +|-----------|---------------|----------| +| `1` | Single instance, no replicas | Development, testing | +| `2` | Primary + 1 replica | Cost-sensitive production | +| `3` | Primary + 2 replicas | **Recommended** for production | + +!!! tip "Why 3 instances?" + Three instances provide quorum-based failover. With 2 instances, the system cannot distinguish between a network partition and a failed primary. With 3 instances, the system can achieve consensus and safely promote a replica. + +## Pod Anti-Affinity + +Pod anti-affinity ensures DocumentDB instances are distributed across failure domains (nodes, zones) for resilience. + +### Zone-Level Distribution (Recommended) + +Distribute instances across availability zones: + +```yaml title="documentdb-zone-affinity.yaml" +apiVersion: documentdb.io/preview +kind: DocumentDB +metadata: + name: my-documentdb + namespace: documentdb +spec: + instancesPerNode: 3 + affinity: + enablePodAntiAffinity: true + topologyKey: topology.kubernetes.io/zone # (1)! +``` + +1. Distributes pods across different availability zones. Requires a cluster with nodes in multiple zones. + +### Node-Level Distribution + +For clusters without multiple zones, distribute across nodes: + +```yaml title="documentdb-node-affinity.yaml" +apiVersion: documentdb.io/preview +kind: DocumentDB +metadata: + name: my-documentdb + namespace: documentdb +spec: + instancesPerNode: 3 + affinity: + enablePodAntiAffinity: true + topologyKey: kubernetes.io/hostname # (1)! +``` + +1. Distributes pods across different nodes. Requires at least 3 nodes in the cluster. + +### Affinity Configuration Reference + +| Field | Type | Description | +|-------|------|-------------| +| `enablePodAntiAffinity` | boolean | Enable/disable pod anti-affinity | +| `topologyKey` | string | Kubernetes topology label for distribution | +| `podAntiAffinityType` | string | `preferred` (default) or `required` | + +!!! warning "Required vs Preferred" + Using `required` anti-affinity prevents scheduling if constraints cannot be met. Use `preferred` (default) to allow scheduling even when ideal placement isn't possible. + +## Automatic Failover + +DocumentDB uses CloudNative-PG's failover mechanism to automatically detect primary failure and promote a replica. No manual intervention is required for local HA failover. + +### Failover Timeline + +```mermaid +sequenceDiagram + participant App as Application + participant P as Primary + participant R as Replica + participant Op as Operator + + Note over P: Primary fails + App->>P: Connection fails + Op->>P: Readiness probe fails + Op->>Op: Wait failoverDelay (default: 0s) + Op->>P: Mark TargetPrimary pending + P->>P: Fast shutdown (up to 30s) + Op->>R: Leader election + R->>R: Promote to primary + Op->>App: Update service endpoint + App->>R: Reconnect to new primary + Note over R: New Primary +``` + +### Failover Timing Parameters + +DocumentDB inherits these timing controls from CloudNative-PG: + +| Parameter | Default | Configurable | Description | +|-----------|---------|--------------|-------------| +| `failoverDelay` | 0 seconds | No | Delay before initiating failover after detecting unhealthy primary | +| `stopDelay` | 30 seconds | **Yes** | Time allowed for graceful PostgreSQL shutdown | +| `switchoverDelay` | 3600 seconds | No | Time for primary to gracefully shutdown during planned switchover | +| `livenessProbeTimeout` | 30 seconds | No | Time allowed for liveness probe response | + +!!! note "Current Configuration" + Currently, only `stopDelay` is configurable via `spec.timeouts.stopDelay`. Other parameters use CloudNative-PG default values. Additional timing parameters may be exposed in future releases. + +### Failover Process + +The failover process occurs in two phases: + +**Phase 1: Primary Shutdown** + +1. Readiness probe detects the primary is unhealthy +2. After `failoverDelay` (default: 0s), operator marks `TargetPrimary` as pending +3. Primary pod initiates fast shutdown (up to `stopDelay` seconds) +4. WAL receivers on replicas stop to prevent timeline discrepancies + +**Phase 2: Promotion** + +1. Leader election selects the most up-to-date replica +2. Selected replica promotes to primary and begins accepting writes +3. Kubernetes service endpoints update to point to new primary +4. Former primary restarts as a replica when recovered + +!!! note "Zero Data Loss" + Because replication is synchronous, a committed write exists on at least one replica before acknowledgment. Failover promotes a replica with all committed data. + +### RTO and RPO Impact + +| Scenario | RTO Impact | RPO Impact | +|----------|------------|------------| +| Fast shutdown succeeds | Seconds to tens of seconds | Zero data loss | +| Fast shutdown times out | Up to `stopDelay` (30s default) | Possible data loss | +| Network partition | Depends on quorum | Zero if quorum maintained | + +!!! tip "Tuning for RTO vs RPO" + Lower `stopDelay` values favor faster recovery (RTO) but may increase data loss risk (RPO). Higher values prioritize data safety but may delay recovery. + +## Testing High Availability + +Verify your HA configuration works correctly. + +### Test 1: Verify Instance Distribution + +```bash +# Check pod distribution across zones/nodes +kubectl get pods -n documentdb -l documentdb.io/cluster=my-documentdb \ + -o custom-columns=NAME:.metadata.name,NODE:.spec.nodeName,ZONE:.metadata.labels.topology\\.kubernetes\\.io/zone +``` + +Expected output shows pods on different nodes/zones: +``` +NAME NODE ZONE +my-documentdb-1 node-1 zone-a +my-documentdb-2 node-2 zone-b +my-documentdb-3 node-3 zone-c +``` + +### Test 2: Simulate Failure + +!!! danger "Production Warning" + Only perform failure testing in non-production environments or during planned maintenance windows. + +```bash +# Delete the primary pod to simulate failure +kubectl delete pod my-documentdb-1 -n documentdb + +# Watch failover (in another terminal) +kubectl get pods -n documentdb -w + +# Check pod status after failover +kubectl get pods -n documentdb -l documentdb.io/cluster=my-documentdb +``` + +### Test 3: Application Connectivity + +```bash +# Get the connection string from DocumentDB status +CONNECTION_STRING=$(kubectl get documentdb my-documentdb -n documentdb -o jsonpath='{.status.connectionString}') +echo "Connection string: $CONNECTION_STRING" + +# Test application can reconnect after failover +mongosh "$CONNECTION_STRING" --eval "print('Connection successful')" +``` + +## Troubleshooting + +### Pods Not Distributing Across Zones + +**Symptom**: Multiple DocumentDB pods scheduled on the same node or zone. + +**Cause**: Anti-affinity set to `preferred` and insufficient nodes/zones available. + +**Solution**: +1. Add more nodes to different zones +2. Or change to `required` anti-affinity (may prevent scheduling if constraints can't be met) + +```bash +# Check node zone labels +kubectl get nodes -L topology.kubernetes.io/zone +``` + +### Failover Taking Too Long + +**Symptom**: Failover takes longer than expected. + +**Possible Causes**: +- `stopDelay` set to high value +- Storage latency affecting shutdown +- Network issues delaying probe failures + +**Solution**: +```bash +# Check operator logs +kubectl logs -n documentdb-operator -l app.kubernetes.io/name=documentdb-operator --tail=100 + +# Check events +kubectl get events -n documentdb --sort-by='.lastTimestamp' | tail -20 +``` + +### Replica Not Catching Up + +**Symptom**: Replica shows increasing replication lag. + +**Possible Causes**: +- Network bandwidth limitation +- Storage I/O bottleneck on replica +- High write load on primary + +**Solution**: +```bash +# Check replica pod resources +kubectl top pod my-documentdb-2 -n documentdb + +# Check pod logs for replication issues +kubectl logs my-documentdb-2 -n documentdb --tail=50 +``` + diff --git a/docs/operator-public-documentation/preview/high-availability/overview.md b/docs/operator-public-documentation/preview/high-availability/overview.md new file mode 100644 index 00000000..1111890a --- /dev/null +++ b/docs/operator-public-documentation/preview/high-availability/overview.md @@ -0,0 +1,169 @@ +--- +title: High Availability Overview +description: Understanding high availability options for DocumentDB on Kubernetes - local HA, multi-region, and multi-cloud deployments. +tags: + - high-availability + - architecture + - disaster-recovery +search: + boost: 2 +--- + +# High Availability Overview + +High availability (HA) ensures your DocumentDB deployment remains accessible and operational despite component failures. This guide covers the HA options available and helps you choose the right approach for your requirements. + +## What is High Availability? + +High availability in DocumentDB means: + +- **Automatic failover**: When a primary instance fails, a replica is automatically promoted +- **Data durability**: Data is replicated across multiple instances before acknowledging writes +- **Minimal downtime**: Recovery happens automatically without manual intervention +- **Continuous operation**: Applications experience brief interruption rather than extended outages + +## Types of High Availability + +DocumentDB supports three levels of high availability, each providing different trade-offs between complexity, cost, and resilience: + +```mermaid +flowchart LR + subgraph Local["Local HA (Single Cluster)"] + direction TB + P1[Primary] --> R1[Replica 1] + P1 --> R2[Replica 2] + end + + subgraph MultiRegion["Multi-Region (Same Cloud)"] + direction TB + subgraph Region1[Region A] + P2[Primary Cluster] + end + subgraph Region2[Region B] + S2[Standby Cluster] + end + P2 -.->|Async Replication| S2 + end + + subgraph MultiCloud["Multi-Cloud"] + direction TB + subgraph Cloud1[Azure] + P3[Primary] + end + subgraph Cloud2[GCP] + S3a[Replica] + end + subgraph Cloud3[AWS] + S3b[Replica] + end + P3 -.->|Cross-Cloud| S3a + P3 -.->|Cross-Cloud| S3b + end + + Local ~~~ MultiRegion ~~~ MultiCloud +``` + +### Local High Availability + +Local HA runs multiple DocumentDB instances within a single Kubernetes cluster, distributed across availability zones. + +| Aspect | Details | +|--------|---------| +| **Scope** | Single Kubernetes cluster | +| **Instances** | 1-3 instances (primary + replicas) | +| **Failover** | Automatic, typically < 30 seconds | +| **Data Loss** | Zero (synchronous replication) | +| **Use Case** | Standard production deployments | + +**Best for:** Most production workloads requiring high availability without geographic distribution. + +[Configure Local HA →](local-ha.md) + +### Multi-Region Deployment + +Multi-region runs DocumentDB clusters across multiple regions within the same cloud provider, connected via the cloud's native networking. + +| Aspect | Details | +|--------|---------| +| **Scope** | Multiple regions, single cloud provider | +| **Networking** | Azure Fleet, VNet peering | +| **Failover** | Manual promotion required | +| **Data Loss** | Minimal (async replication lag) | +| **Use Case** | Disaster recovery, data locality | + +**Best for:** Disaster recovery requirements, regulatory compliance requiring data in specific regions, or reducing latency for geographically distributed users. + +### Multi-Cloud Deployment + +Multi-cloud runs DocumentDB across different cloud providers (Azure, AWS, GCP), connected via service mesh. + +| Aspect | Details | +|--------|---------| +| **Scope** | Multiple cloud providers | +| **Networking** | Istio service mesh | +| **Failover** | Manual promotion required | +| **Data Loss** | Minimal (async replication lag) | +| **Use Case** | Vendor independence, maximum resilience | + +**Best for:** Organizations requiring cloud vendor independence, maximum disaster resilience, or hybrid cloud strategies. + +## RTO and RPO Concepts + +When planning for high availability, understand these key metrics: + +### Recovery Time Objective (RTO) + +**RTO** is the maximum acceptable time your application can be unavailable after a failure. + +| HA Type | Typical RTO | +|---------|-------------| +| Local HA | < 30 seconds | +| Multi-Region | Minutes (manual failover) | +| Multi-Cloud | Minutes (manual failover) | + +### Recovery Point Objective (RPO) + +**RPO** is the maximum acceptable amount of data loss measured in time. + +| HA Type | Typical RPO | +|---------|-------------| +| Local HA | 0 (synchronous) | +| Multi-Region | Seconds (replication lag) | +| Multi-Cloud | Seconds (replication lag) | + +## Decision Tree + +Use this guide to select the appropriate HA strategy: + +```mermaid +flowchart TD + A[Start] --> B{Need geographic
distribution?} + B -->|No| C{Need zone-level
resilience?} + B -->|Yes| D{Need cloud vendor
independence?} + + C -->|Yes| E[Local HA
Multiple instances] + C -->|No| F[Single instance
Development only] + + D -->|Yes| G[Multi-Cloud
Istio mesh] + D -->|No| H[Multi-Region
Same cloud provider] + + E --> I[Configure instancesPerNode: 3] + F --> J[Configure instancesPerNode: 1] + G --> K[See Multi-Cloud Guide] + H --> L[See Multi-Region Guide] +``` + +## Trade-offs Summary + +| Factor | Local HA | Multi-Region | Multi-Cloud | +|--------|----------|--------------|-------------| +| **Complexity** | Low | Medium | High | +| **Cost** | $ | $$ | $$$ | +| **RTO** | Seconds | Minutes | Minutes | +| **RPO** | Zero | Replication lag | Replication lag | +| **Blast radius** | Zone outage | Region outage | Cloud outage | +| **Network latency** | Minimal | Regional | Variable | + +## Next Steps + +- [Configure Local HA](local-ha.md) - Set up high availability within a single cluster diff --git a/operator/cnpg-plugins/sidecar-injector/internal/config/config_test.go b/operator/cnpg-plugins/sidecar-injector/internal/config/config_test.go new file mode 100644 index 00000000..3e8f3205 --- /dev/null +++ b/operator/cnpg-plugins/sidecar-injector/internal/config/config_test.go @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package config + +import ( + "testing" + + "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/common" + corev1 "k8s.io/api/core/v1" +) + +func TestParsePullPolicy(t *testing.T) { + tests := []struct { + name string + input string + expected corev1.PullPolicy + }{ + {"Always", "Always", corev1.PullAlways}, + {"Never", "Never", corev1.PullNever}, + {"IfNotPresent", "IfNotPresent", corev1.PullIfNotPresent}, + {"empty string returns empty", "", ""}, + {"invalid value returns empty", "invalid", ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := parsePullPolicy(tt.input) + if result != tt.expected { + t.Errorf("parsePullPolicy(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestApplyDefaults(t *testing.T) { + t.Run("sets default gateway image when empty", func(t *testing.T) { + config := &Configuration{} + config.applyDefaults() + expected := "ghcr.io/documentdb/documentdb-kubernetes-operator/gateway:0.110.0" + if config.GatewayImage != expected { + t.Errorf("expected %q, got %q", expected, config.GatewayImage) + } + }) + + t.Run("sets IfNotPresent when pull policy is empty", func(t *testing.T) { + config := &Configuration{} + config.applyDefaults() + if config.GatewayImagePullPolicy != corev1.PullIfNotPresent { + t.Errorf("expected IfNotPresent, got %q", config.GatewayImagePullPolicy) + } + }) + + t.Run("preserves explicit pull policy", func(t *testing.T) { + config := &Configuration{GatewayImagePullPolicy: corev1.PullNever} + config.applyDefaults() + if config.GatewayImagePullPolicy != corev1.PullNever { + t.Errorf("expected Never, got %q", config.GatewayImagePullPolicy) + } + }) +} + +func TestFromParameters(t *testing.T) { + t.Run("pull policy from parameters", func(t *testing.T) { + helper := &common.Plugin{Parameters: map[string]string{ + "gatewayImagePullPolicy": "Never", + }} + config, errs := FromParameters(helper) + if len(errs) != 0 { + t.Fatalf("unexpected validation errors: %v", errs) + } + if config.GatewayImagePullPolicy != corev1.PullNever { + t.Errorf("GatewayImagePullPolicy = %q, want Never", config.GatewayImagePullPolicy) + } + }) + + t.Run("defaults to IfNotPresent when not set", func(t *testing.T) { + helper := &common.Plugin{Parameters: map[string]string{}} + config, errs := FromParameters(helper) + if len(errs) != 0 { + t.Fatalf("unexpected validation errors: %v", errs) + } + if config.GatewayImagePullPolicy != corev1.PullIfNotPresent { + t.Errorf("GatewayImagePullPolicy = %q, want IfNotPresent", config.GatewayImagePullPolicy) + } + }) +} + +func TestToParametersRoundTrip(t *testing.T) { + original := &Configuration{ + GatewayImage: "my-image:latest", + GatewayImagePullPolicy: corev1.PullNever, + } + original.applyDefaults() + + params, err := original.ToParameters() + if err != nil { + t.Fatalf("ToParameters() error: %v", err) + } + + helper := &common.Plugin{Parameters: params} + restored, errs := FromParameters(helper) + if len(errs) != 0 { + t.Fatalf("unexpected validation errors: %v", errs) + } + if restored.GatewayImagePullPolicy != original.GatewayImagePullPolicy { + t.Errorf("round-trip pull policy = %q, want %q", restored.GatewayImagePullPolicy, original.GatewayImagePullPolicy) + } + if restored.GatewayImage != original.GatewayImage { + t.Errorf("round-trip gateway image = %q, want %q", restored.GatewayImage, original.GatewayImage) + } +} diff --git a/plugins/wal-replica/README.md b/plugins/wal-replica/README.md index 1967f50b..a7cb7e69 100644 --- a/plugins/wal-replica/README.md +++ b/plugins/wal-replica/README.md @@ -2,6 +2,38 @@ This plugin creates a standalone WAL receiver deployment alongside a [CloudNativePG](https://github.com/cloudnative-pg/cloudnative-pg/) cluster. It automatically provisions a Deployment named `-wal-receiver` that continuously streams Write-Ahead Log (WAL) files from the primary PostgreSQL cluster using `pg_receivewal`, with support for both synchronous and asynchronous replication modes. +## Architecture + +The plugin uses the [CNPG-I](https://github.com/cloudnative-pg/cnpg-i) (CloudNativePG Interface) gRPC protocol to extend CloudNativePG without forking the operator. + +``` + + Kubernetes Cluster │ + │ + gRPC/TLS ┌────────────────────┐ │ ┌────────── + │ CloudNativePG│◄─────────────►│ WAL Replica Plugin │ │ + Operator │ (this plugin) │ ││ + └──────┬───────┘ └────────┬───────────┘ │ + │ │ │ + │ manages │ creates │ + ▼ ▼ │ + ┌──────────────┐ ┌────────────────────┐ │ + │ CNPG Cluster │ WAL stream │ WAL Receiver │ │ + │ (Primary PG) │◄─────────────│ Deployment + PVC │ │ + │ pg_receivewal (└──────────────┘)└────────── + +``` + +### CNPG-I Interfaces Implemented + +| Interface | Purpose | +|-----------|---------| +| **identity** | Declares plugin metadata and advertised capabilities | +| **operator** | Validates and mutates Cluster resources via webhooks | +| **reconciler** | Post-reconcile hook creates/updates the WAL receiver Deployment and PVC | + +> **Note:** The `SetStatusInCluster` capability is currently disabled due to an [oscillation bug](https://github.com/documentdb/documentdb-kubernetes-operator/pull/74) where the enabled field alternates on every reconciliation. The `MutateCluster` webhook is registered but not fully implemented upstream in CNPG as of v1.28; defaults are applied in the reconciler as a workaround. + ## Features - **Automated WAL Streaming**: Continuously receives and stores WAL files from the primary cluster @@ -9,7 +41,9 @@ This plugin creates a standalone WAL receiver deployment alongside a [CloudNativ - **TLS Security**: Uses cluster certificates for secure replication connections - **Replication Slot Management**: Automatically creates and manages a dedicated replication slot (`wal_replica`) - **Synchronous Replication Support**: Configurable synchronous/asynchronous replication modes -- **Cluster Lifecycle Management**: Automatically manages resources with proper owner references +- **Health Probes**: Liveness and readiness probes on the WAL receiver container +- **Cluster Lifecycle Management**: Proper OwnerReferences ensure resources are cleaned up when the cluster is deleted +- **Deployment Updates**: Configuration changes are automatically patched onto the existing Deployment ## Configuration @@ -22,7 +56,7 @@ metadata: name: my-cluster spec: instances: 3 - + plugins: - name: cnpg-i-wal-replica.documentdb.io parameters: @@ -31,9 +65,11 @@ spec: synchronous: "active" walDirectory: "/var/lib/postgresql/wal" walPVCSize: "20Gi" + verbose: "true" + compression: "0" replicationSlots: - synchronizeReplicas: + synchronizeReplicas: enabled: true storage: @@ -46,145 +82,61 @@ spec: |-----------|------|---------|-------------| | `image` | string | Cluster status image | Container image providing `pg_receivewal` binary | | `replicationHost` | string | `-rw` | Primary host endpoint for WAL streaming | -| `synchronous` | string | `inactive` | Replication mode: `active` (synchronous) or `inactive` (asynchronous) | +| `synchronous` | string | `inactive` | Replication mode: `active` or `inactive` | | `walDirectory` | string | `/var/lib/postgresql/wal` | Directory path for storing received WAL files | | `walPVCSize` | string | `10Gi` | Size of the PersistentVolumeClaim for WAL storage | +| `verbose` | string | `true` | Enable verbose `pg_receivewal` output (`true` / `false`) | +| `compression` | string | `0` | Compression level for WAL files (0-9, 0=disabled) | -#### Synchronous Modes +## Deployment -- **`active`**: Enables synchronous replication with `--synchronous` flag -- **`inactive`**: Standard asynchronous replication (default) +The plugin runs as an independent Deployment and is discovered by CNPG via a Kubernetes Service with the `cnpgi.io/pluginName` label: -## Architecture - -The plugin creates the following Kubernetes resources: - -1. **Deployment**: `-wal-receiver` - - Single replica pod running `pg_receivewal` - - Configured with proper security context (user: 105, group: 103) - - Automatic restart policy for high availability - -2. **PersistentVolumeClaim**: `-wal-receiver` - - Stores received WAL files persistently - - Uses `ReadWriteOnce` access mode - - Configurable size via `walPVCSize` parameter - -3. **Volume Mounts**: - - WAL storage: Mounted at configured `walDirectory` - - TLS certificates: Mounted from cluster certificate secrets - - CA certificates: Mounted for SSL verification - -## Security - -The plugin implements comprehensive security measures: - -- **TLS Encryption**: All replication connections use SSL/TLS -- **Certificate Management**: Automatically mounts cluster CA and client certificates -- **User Privileges**: Runs with dedicated PostgreSQL user and group IDs -- **Connection Authentication**: Uses `streaming_replica` user with certificate-based auth - -## Prerequisites +```bash +# Deploy using kustomize +kubectl apply -k kubernetes/ +``` -- CloudNativePG operator installed and running -- CNPG-I (CloudNativePG Interface) framework deployed -- Cluster with enabled replication slots synchronization -- Sufficient storage for WAL files retention +See the `kubernetes/` directory for the full set of manifests (Deployment, Service, RBAC, TLS certificates). -## Installation +## Development -### Building from Source +### Build ```bash -# Clone the repository -git clone https://github.com/documentdb/cnpg-i-wal-replica -cd cnpg-i-wal-replica - -# Build the binary go build -o bin/cnpg-i-wal-replica main.go ``` -### Using Docker +### Test ```bash -# Build container image -docker build -t cnpg-i-wal-replica:latest . -``` - -### Deployment Scripts - -```bash -# Make scripts executable -chmod +x scripts/build.sh scripts/run.sh - -# Build and run -./scripts/build.sh -./scripts/run.sh +go test ./... ``` -## Monitoring and Observability - -The WAL receiver pod provides verbose logging when enabled, including: - -- Connection status to primary cluster -- WAL file reception progress -- Replication slot status -- SSL/TLS connection details - -## Examples - -See the `doc/examples/` directory for complete cluster configurations: - -- [`cluster-example.yaml`](doc/examples/cluster-example.yaml): Basic configuration -- [`cluster-example-no-parameters.yaml`](doc/examples/cluster-example-no-parameters.yaml): Default settings -- [`cluster-example-with-mistake.yaml`](doc/examples/cluster-example-with-mistake.yaml): Common configuration errors - -## Development - ### Project Structure ``` -├── cmd/plugin/ # Plugin command-line interface -├── internal/ -│ ├── config/ # Configuration management -│ ├── identity/ # Plugin identity and metadata -│ ├── k8sclient/ # Kubernetes client utilities -│ ├── operator/ # Operator implementations -│ └── reconciler/ # Resource reconciliation logic -├── kubernetes/ # Kubernetes manifests -├── pkg/metadata/ # Plugin metadata and constants -└── scripts/ # Build and deployment scripts -``` - -### Running Tests - -```bash -go test ./... + cmd/plugin/ # Plugin command-line interface + internal/ + config/ # Configuration management and validation ├ + ├── identity/ # Plugin identity and capabilities + ├── k8sclient/ # Kubernetes client utilities + ├── operator/ # Operator hooks (validate, mutate, status) + └── reconciler/ # Reconciliation logic (Deployment + PVC management) + kubernetes/ # Kubernetes manifests + pkg/metadata/ # Plugin metadata constants + scripts/ # Build and deployment scripts ``` See [`doc/development.md`](doc/development.md) for detailed development guidelines. -## Limitations and Future Enhancements +## Limitations and Known Issues -### Current Limitations - -- Fixed compression level (disabled: `--compress 0`) +- `MutateCluster` is not fully implemented upstream in CNPG v1.28; defaults are applied in the reconciler +- `SetStatusInCluster` is disabled due to status oscillation bug +- Fixed replication slot name (`wal_replica`) - No built-in WAL retention/cleanup policies -- Limited resource configuration options - -### Planned Enhancements - -- [ ] Configurable resource requests and limits -- [ ] WAL retention and garbage collection policies -- [ ] Health checks and readiness probes -- [ ] Metrics exposure for monitoring integration -- [ ] Multi-zone/region WAL archiving support -- [ ] Backup integration with existing CNPG backup strategies ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. - -## Contributing - -Contributions are welcome! Please read [CONTRIBUTING.md](../../CONTRIBUTING.md) for guidelines on how to contribute to this project. - diff --git a/plugins/wal-replica/internal/config/config.go b/plugins/wal-replica/internal/config/config.go index e9ee13a3..e5305a67 100644 --- a/plugins/wal-replica/internal/config/config.go +++ b/plugins/wal-replica/internal/config/config.go @@ -4,117 +4,158 @@ package config import ( - "fmt" - "strings" - - cnpgv1 "github.com/cloudnative-pg/api/pkg/api/v1" - "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/common" - "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/validation" - "github.com/cloudnative-pg/cnpg-i/pkg/operator" - "k8s.io/apimachinery/pkg/api/resource" +"fmt" +"strconv" +"strings" + +cnpgv1 "github.com/cloudnative-pg/api/pkg/api/v1" +"github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/common" +"github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/validation" +"github.com/cloudnative-pg/cnpg-i/pkg/operator" +"k8s.io/apimachinery/pkg/api/resource" ) // Plugin parameter keys const ( - ImageParam = "image" // string - ReplicationHostParam = "replicationHost" // primary host - SynchronousParam = "synchronous" // enum: Active, Inactive, Unset - WalDirectoryParam = "walDirectory" // directory where WAL is stored - WalPVCSize = "walPVCSize" // Size of the PVC for WAL storage +ImageParam = "image" // string: container image for the WAL receiver +ReplicationHostParam = "replicationHost" // string: primary host for WAL streaming +SynchronousParam = "synchronous" // enum: active, inactive +WalDirectoryParam = "walDirectory" // string: directory where WAL files are stored +WalPVCSizeParam = "walPVCSize" // string: PVC size (e.g., "10Gi") +VerboseParam = "verbose" // bool string: enable verbose pg_receivewal output +CompressionParam = "compression" // int string: compression level (0=disabled) ) // SynchronousMode represents the synchronous replication mode type SynchronousMode string const ( - SynchronousUnset SynchronousMode = "" - SynchronousActive SynchronousMode = "active" - SynchronousInactive SynchronousMode = "inactive" +SynchronousUnset SynchronousMode = "" +SynchronousActive SynchronousMode = "active" +SynchronousInactive SynchronousMode = "inactive" ) const ( - defaultWalDir = "/var/lib/postgresql/wal" - defaultSynchronousMode = SynchronousInactive +defaultWalDir = "/var/lib/postgresql/wal" +defaultSynchronousMode = SynchronousInactive +defaultWalPVCSize = "10Gi" +defaultVerbose = true +defaultCompression = 0 ) // Configuration represents the plugin configuration parameters controlling the wal receiver pod type Configuration struct { - Image string - ReplicationHost string - Synchronous SynchronousMode - WalDirectory string - WalPVCSize string +Image string +ReplicationHost string +Synchronous SynchronousMode +WalDirectory string +WalPVCSize string +Verbose bool +Compression int } -// FromParameters builds a plugin configuration from the configuration parameters -func FromParameters(helper *common.Plugin) *Configuration { - cfg := &Configuration{} - cfg.Image = helper.Parameters[ImageParam] - cfg.ReplicationHost = helper.Parameters[ReplicationHostParam] - cfg.Synchronous = SynchronousMode(strings.ToLower(helper.Parameters[SynchronousParam])) - cfg.WalDirectory = helper.Parameters[WalDirectoryParam] - cfg.WalPVCSize = helper.Parameters[WalPVCSize] - return cfg +// FromParameters builds a plugin configuration from the configuration parameters. +// Returns the configuration and any validation errors encountered during parsing. +func FromParameters(helper *common.Plugin) (*Configuration, []*operator.ValidationError) { +validationErrors := ValidateParams(helper) + +cfg := &Configuration{} +cfg.Image = helper.Parameters[ImageParam] +cfg.ReplicationHost = helper.Parameters[ReplicationHostParam] +cfg.Synchronous = SynchronousMode(strings.ToLower(helper.Parameters[SynchronousParam])) +cfg.WalDirectory = helper.Parameters[WalDirectoryParam] +cfg.WalPVCSize = helper.Parameters[WalPVCSizeParam] + +if raw, ok := helper.Parameters[VerboseParam]; ok && raw != "" { +cfg.Verbose = strings.EqualFold(raw, "true") +} else { +cfg.Verbose = defaultVerbose +} + +if raw, ok := helper.Parameters[CompressionParam]; ok && raw != "" { +if v, err := strconv.Atoi(raw); err == nil { +cfg.Compression = v +} +} + +return cfg, validationErrors } // ValidateChanges validates the changes between the old configuration to the new configuration func ValidateChanges(_ *Configuration, _ *Configuration, _ *common.Plugin) []*operator.ValidationError { - return nil +return nil } -// ToParameters serialize the configuration back to plugin parameters +// ToParameters serializes the configuration back to plugin parameters func (c *Configuration) ToParameters() (map[string]string, error) { - params := map[string]string{} - params[ImageParam] = c.Image - params[ReplicationHostParam] = c.ReplicationHost - params[SynchronousParam] = string(c.Synchronous) - params[WalDirectoryParam] = c.WalDirectory - params[WalPVCSize] = c.WalPVCSize - return params, nil +params := map[string]string{ +ImageParam: c.Image, +ReplicationHostParam: c.ReplicationHost, +SynchronousParam: string(c.Synchronous), +WalDirectoryParam: c.WalDirectory, +WalPVCSizeParam: c.WalPVCSize, +VerboseParam: strconv.FormatBool(c.Verbose), +CompressionParam: strconv.Itoa(c.Compression), +} +return params, nil } // ValidateParams ensures that the provided parameters are valid func ValidateParams(helper *common.Plugin) []*operator.ValidationError { - validationErrors := make([]*operator.ValidationError, 0) - - // If present, must be valid - if raw, present := helper.Parameters[SynchronousParam]; present && raw != "" { - switch SynchronousMode(strings.ToLower(raw)) { - case SynchronousActive, SynchronousInactive: - // valid value - default: - validationErrors = append(validationErrors, validation.BuildErrorForParameter(helper, SynchronousParam, - fmt.Sprintf("Invalid value '%s'. Must be 'active' or 'inactive'", raw))) - } - } - - // If present, Wal size must be valid - if raw, present := helper.Parameters[WalPVCSize]; present && raw != "" { - if _, err := resource.ParseQuantity(raw); err != nil { - validationErrors = append(validationErrors, validation.BuildErrorForParameter(helper, WalPVCSize, err.Error())) - } - } - - return validationErrors -} - -// applyDefaults fills the configuration with the defaults -// We know that replicationhost and sync are valid already +validationErrors := make([]*operator.ValidationError, 0) + +if raw, present := helper.Parameters[SynchronousParam]; present && raw != "" { +switch SynchronousMode(strings.ToLower(raw)) { +case SynchronousActive, SynchronousInactive: +// valid +default: +validationErrors = append(validationErrors, validation.BuildErrorForParameter(helper, SynchronousParam, +fmt.Sprintf("invalid value '%s'; must be 'active' or 'inactive'", raw))) +} +} + +if raw, present := helper.Parameters[WalPVCSizeParam]; present && raw != "" { +if _, err := resource.ParseQuantity(raw); err != nil { +validationErrors = append(validationErrors, validation.BuildErrorForParameter(helper, WalPVCSizeParam, err.Error())) +} +} + +if raw, present := helper.Parameters[VerboseParam]; present && raw != "" { +if !strings.EqualFold(raw, "true") && !strings.EqualFold(raw, "false") { +validationErrors = append(validationErrors, validation.BuildErrorForParameter(helper, VerboseParam, +fmt.Sprintf("invalid value '%s'; must be 'true' or 'false'", raw))) +} +} + +if raw, present := helper.Parameters[CompressionParam]; present && raw != "" { +v, err := strconv.Atoi(raw) +if err != nil { +validationErrors = append(validationErrors, validation.BuildErrorForParameter(helper, CompressionParam, +fmt.Sprintf("invalid value '%s'; must be an integer", raw))) +} else if v < 0 || v > 9 { +validationErrors = append(validationErrors, validation.BuildErrorForParameter(helper, CompressionParam, +fmt.Sprintf("invalid value '%d'; must be between 0 and 9", v))) +} +} + +return validationErrors +} + +// ApplyDefaults fills the configuration with default values func (c *Configuration) ApplyDefaults(cluster *cnpgv1.Cluster) { - if c.Image == "" { - c.Image = cluster.Status.Image - } - if c.ReplicationHost == "" { - // Only doing reads, but want to make sure we get a primary - c.ReplicationHost = cluster.Status.WriteService - } - if c.WalDirectory == "" { - c.WalDirectory = defaultWalDir - } - if c.Synchronous == SynchronousUnset { - c.Synchronous = defaultSynchronousMode - } - if c.WalPVCSize == "" { - c.WalPVCSize = "10Gi" - } +if c.Image == "" { +c.Image = cluster.Status.Image +} +if c.ReplicationHost == "" { +c.ReplicationHost = cluster.Status.WriteService +} +if c.WalDirectory == "" { +c.WalDirectory = defaultWalDir +} +if c.Synchronous == SynchronousUnset { +c.Synchronous = defaultSynchronousMode +} +if c.WalPVCSize == "" { +c.WalPVCSize = defaultWalPVCSize +} } diff --git a/plugins/wal-replica/internal/config/config_test.go b/plugins/wal-replica/internal/config/config_test.go new file mode 100644 index 00000000..6a9ff248 --- /dev/null +++ b/plugins/wal-replica/internal/config/config_test.go @@ -0,0 +1,226 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package config + +import ( +"testing" + +cnpgv1 "github.com/cloudnative-pg/api/pkg/api/v1" +"github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/common" +metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +"k8s.io/utils/pointer" +) + +func newHelper(params map[string]string) *common.Plugin { +cluster := cnpgv1.Cluster{ +ObjectMeta: metav1.ObjectMeta{Name: "test-cluster", Namespace: "default"}, +Spec: cnpgv1.ClusterSpec{ +Plugins: []cnpgv1.PluginConfiguration{ +{ +Name: "cnpg-i-wal-replica.documentdb.io", +Enabled: pointer.Bool(true), +Parameters: params, +}, +}, +}, +} +return common.NewPlugin(cluster, "cnpg-i-wal-replica.documentdb.io") +} + +func TestFromParameters_Defaults(t *testing.T) { +helper := newHelper(map[string]string{}) +cfg, errs := FromParameters(helper) + +if len(errs) != 0 { +t.Fatalf("expected no validation errors, got %d", len(errs)) +} +if cfg.Verbose != true { +t.Errorf("expected verbose=true by default, got %v", cfg.Verbose) +} +if cfg.Compression != 0 { +t.Errorf("expected compression=0 by default, got %d", cfg.Compression) +} +if cfg.Image != "" { +t.Errorf("expected empty image before ApplyDefaults, got %q", cfg.Image) +} +} + +func TestFromParameters_WithValues(t *testing.T) { +helper := newHelper(map[string]string{ +ImageParam: "postgres:16", +ReplicationHostParam: "my-cluster-rw", +SynchronousParam: "Active", +WalDirectoryParam: "/custom/wal", +WalPVCSizeParam: "50Gi", +VerboseParam: "false", +CompressionParam: "5", +}) +cfg, errs := FromParameters(helper) + +if len(errs) != 0 { +t.Fatalf("expected no validation errors, got %d", len(errs)) +} +if cfg.Image != "postgres:16" { +t.Errorf("expected image postgres:16, got %q", cfg.Image) +} +if cfg.Synchronous != SynchronousActive { +t.Errorf("expected synchronous=active, got %q", cfg.Synchronous) +} +if cfg.WalPVCSize != "50Gi" { +t.Errorf("expected walPVCSize=50Gi, got %q", cfg.WalPVCSize) +} +if cfg.Verbose != false { +t.Errorf("expected verbose=false, got %v", cfg.Verbose) +} +if cfg.Compression != 5 { +t.Errorf("expected compression=5, got %d", cfg.Compression) +} +} + +func TestFromParameters_InvalidSynchronous(t *testing.T) { +helper := newHelper(map[string]string{ +SynchronousParam: "bogus", +}) +_, errs := FromParameters(helper) + +if len(errs) != 1 { +t.Fatalf("expected 1 validation error, got %d", len(errs)) +} +} + +func TestFromParameters_InvalidCompression(t *testing.T) { +tests := []struct { +name string +value string +}{ +{"not a number", "abc"}, +{"too high", "10"}, +{"negative", "-1"}, +} +for _, tt := range tests { +t.Run(tt.name, func(t *testing.T) { +helper := newHelper(map[string]string{ +CompressionParam: tt.value, +}) +_, errs := FromParameters(helper) +if len(errs) != 1 { +t.Fatalf("expected 1 validation error for %q, got %d", tt.value, len(errs)) +} +}) +} +} + +func TestFromParameters_InvalidVerbose(t *testing.T) { +helper := newHelper(map[string]string{ +VerboseParam: "yes", +}) +_, errs := FromParameters(helper) + +if len(errs) != 1 { +t.Fatalf("expected 1 validation error, got %d", len(errs)) +} +} + +func TestFromParameters_InvalidWalPVCSize(t *testing.T) { +helper := newHelper(map[string]string{ +WalPVCSizeParam: "not-a-quantity", +}) +_, errs := FromParameters(helper) + +if len(errs) != 1 { +t.Fatalf("expected 1 validation error, got %d", len(errs)) +} +} + +func TestApplyDefaults(t *testing.T) { +cfg := &Configuration{} +cluster := &cnpgv1.Cluster{ +Status: cnpgv1.ClusterStatus{ +Image: "postgres:16-default", +WriteService: "test-cluster-rw.default.svc", +}, +} + +cfg.ApplyDefaults(cluster) + +if cfg.Image != "postgres:16-default" { +t.Errorf("expected default image, got %q", cfg.Image) +} +if cfg.ReplicationHost != "test-cluster-rw.default.svc" { +t.Errorf("expected default replicationHost, got %q", cfg.ReplicationHost) +} +if cfg.WalDirectory != defaultWalDir { +t.Errorf("expected default walDirectory, got %q", cfg.WalDirectory) +} +if cfg.Synchronous != defaultSynchronousMode { +t.Errorf("expected default synchronous, got %q", cfg.Synchronous) +} +if cfg.WalPVCSize != defaultWalPVCSize { +t.Errorf("expected default walPVCSize, got %q", cfg.WalPVCSize) +} +} + +func TestApplyDefaults_NoOverwrite(t *testing.T) { +cfg := &Configuration{ +Image: "custom:latest", +ReplicationHost: "custom-host", +WalDirectory: "/custom", +Synchronous: SynchronousActive, +WalPVCSize: "100Gi", +} +cluster := &cnpgv1.Cluster{ +Status: cnpgv1.ClusterStatus{ +Image: "should-not-be-used", +WriteService: "should-not-be-used", +}, +} + +cfg.ApplyDefaults(cluster) + +if cfg.Image != "custom:latest" { +t.Errorf("expected custom image preserved, got %q", cfg.Image) +} +if cfg.ReplicationHost != "custom-host" { +t.Errorf("expected custom host preserved, got %q", cfg.ReplicationHost) +} +if cfg.WalPVCSize != "100Gi" { +t.Errorf("expected custom PVC size preserved, got %q", cfg.WalPVCSize) +} +} + +func TestToParameters_Roundtrip(t *testing.T) { +original := &Configuration{ +Image: "postgres:16", +ReplicationHost: "host-rw", +Synchronous: SynchronousActive, +WalDirectory: "/wal", +WalPVCSize: "20Gi", +Verbose: true, +Compression: 3, +} + +params, err := original.ToParameters() +if err != nil { +t.Fatalf("ToParameters failed: %v", err) +} + +helper := newHelper(params) +roundtripped, errs := FromParameters(helper) +if len(errs) != 0 { +t.Fatalf("unexpected validation errors: %v", errs) +} + +if roundtripped.Image != original.Image { +t.Errorf("image mismatch: %q vs %q", roundtripped.Image, original.Image) +} +if roundtripped.Synchronous != original.Synchronous { +t.Errorf("synchronous mismatch: %q vs %q", roundtripped.Synchronous, original.Synchronous) +} +if roundtripped.Verbose != original.Verbose { +t.Errorf("verbose mismatch: %v vs %v", roundtripped.Verbose, original.Verbose) +} +if roundtripped.Compression != original.Compression { +t.Errorf("compression mismatch: %d vs %d", roundtripped.Compression, original.Compression) +} +} diff --git a/plugins/wal-replica/internal/identity/impl_test.go b/plugins/wal-replica/internal/identity/impl_test.go new file mode 100644 index 00000000..3d156275 --- /dev/null +++ b/plugins/wal-replica/internal/identity/impl_test.go @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package identity + +import ( +"context" +"testing" + +"github.com/cloudnative-pg/cnpg-i/pkg/identity" +"github.com/documentdb/cnpg-i-wal-replica/pkg/metadata" +) + +func TestGetPluginMetadata(t *testing.T) { +impl := Implementation{} +resp, err := impl.GetPluginMetadata(context.Background(), &identity.GetPluginMetadataRequest{}) +if err != nil { +t.Fatalf("unexpected error: %v", err) +} +if resp.Name != metadata.PluginName { +t.Errorf("expected plugin name %q, got %q", metadata.PluginName, resp.Name) +} +} + +func TestGetPluginCapabilities(t *testing.T) { +impl := Implementation{} +resp, err := impl.GetPluginCapabilities(context.Background(), &identity.GetPluginCapabilitiesRequest{}) +if err != nil { +t.Fatalf("unexpected error: %v", err) +} + +if len(resp.Capabilities) == 0 { +t.Fatal("expected at least one capability") +} + +hasOperator := false +hasReconciler := false +for _, cap := range resp.Capabilities { +svc := cap.GetService() +if svc == nil { +continue +} +switch svc.Type { +case identity.PluginCapability_Service_TYPE_OPERATOR_SERVICE: +hasOperator = true +case identity.PluginCapability_Service_TYPE_RECONCILER_HOOKS: +hasReconciler = true +} +} + +if !hasOperator { +t.Error("expected TYPE_OPERATOR_SERVICE capability") +} +if !hasReconciler { +t.Error("expected TYPE_RECONCILER_HOOKS capability") +} +} + +func TestProbe(t *testing.T) { +impl := Implementation{} +resp, err := impl.Probe(context.Background(), &identity.ProbeRequest{}) +if err != nil { +t.Fatalf("unexpected error: %v", err) +} +if !resp.Ready { +t.Error("expected probe to report ready") +} +} diff --git a/plugins/wal-replica/internal/operator/impl.go b/plugins/wal-replica/internal/operator/impl.go index efd6d119..97894737 100644 --- a/plugins/wal-replica/internal/operator/impl.go +++ b/plugins/wal-replica/internal/operator/impl.go @@ -9,12 +9,12 @@ import ( "github.com/cloudnative-pg/cnpg-i/pkg/operator" ) -// Implementation is the implementation of the identity service +// Implementation is the implementation of the operator service type Implementation struct { operator.OperatorServer } -// GetCapabilities gets the capabilities of this operator lifecycle hook +// GetCapabilities gets the capabilities of this operator hook func (Implementation) GetCapabilities( context.Context, *operator.OperatorCapabilitiesRequest, @@ -35,18 +35,15 @@ func (Implementation) GetCapabilities( }, }, }, - /* TODO re-add if we need status or can figure out the oscillation bug - { - Type: &operator.OperatorCapability_Rpc{ - Rpc: &operator.OperatorCapability_RPC{ - Type: operator.OperatorCapability_RPC_TYPE_SET_STATUS_IN_CLUSTER, - }, - }, - }, - */ + // TYPE_SET_STATUS_IN_CLUSTER is disabled due to an oscillation bug + // where the enabled field alternates on every reconciliation cycle. + // Re-enable once the root cause is identified and fixed upstream. { Type: &operator.OperatorCapability_Rpc{ Rpc: &operator.OperatorCapability_RPC{ + // NOTE: MutateCluster is not fully implemented on the CNPG operator + // side as of v1.28. Defaults are applied via ApplyDefaults in the + // reconciler as a workaround. Type: operator.OperatorCapability_RPC_TYPE_MUTATE_CLUSTER, }, }, diff --git a/plugins/wal-replica/internal/operator/mutations.go b/plugins/wal-replica/internal/operator/mutations.go index 68417ecf..8480be94 100644 --- a/plugins/wal-replica/internal/operator/mutations.go +++ b/plugins/wal-replica/internal/operator/mutations.go @@ -5,6 +5,7 @@ package operator import ( "context" + "fmt" "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/common" "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/decoder" @@ -17,6 +18,9 @@ import ( ) // MutateCluster is called to mutate a cluster with the defaulting webhook. +// NOTE: MutateCluster is not fully implemented on the CNPG operator side as of CNPG 1.28. +// See: https://github.com/documentdb/documentdb-kubernetes-operator/pull/74#issuecomment-3518389125 +// Defaults are applied via ApplyDefaults in the reconciler as a workaround. func (Implementation) MutateCluster( ctx context.Context, request *operator.OperatorMutateClusterRequest, @@ -33,15 +37,19 @@ func (Implementation) MutateCluster( metadata.PluginName, ) - config := config.FromParameters(helper) + cfg, valErrs := config.FromParameters(helper) + if len(valErrs) > 0 { + return nil, fmt.Errorf("invalid plugin configuration: %s", valErrs[0].Message) + } + mutatedCluster := cluster.DeepCopy() if helper.PluginIndex >= 0 { if mutatedCluster.Spec.Plugins[helper.PluginIndex].Parameters == nil { mutatedCluster.Spec.Plugins[helper.PluginIndex].Parameters = make(map[string]string) } - config.ApplyDefaults(cluster) + cfg.ApplyDefaults(cluster) - mutatedCluster.Spec.Plugins[helper.PluginIndex].Parameters, err = config.ToParameters() + mutatedCluster.Spec.Plugins[helper.PluginIndex].Parameters, err = cfg.ToParameters() if err != nil { return nil, err } diff --git a/plugins/wal-replica/internal/operator/status.go b/plugins/wal-replica/internal/operator/status.go index 1629de99..12a2467d 100644 --- a/plugins/wal-replica/internal/operator/status.go +++ b/plugins/wal-replica/internal/operator/status.go @@ -18,10 +18,15 @@ import ( "github.com/documentdb/cnpg-i-wal-replica/pkg/metadata" ) +// Status represents the plugin status reported in the CNPG Cluster status. type Status struct { Enabled bool `json:"enabled"` } +// SetStatusInCluster reports plugin status in the Cluster resource. +// NOTE: This capability is currently disabled in GetCapabilities (impl.go) due to a +// known oscillation bug where the `enabled` field alternates on every reconciliation. +// See: https://github.com/documentdb/documentdb-kubernetes-operator/pull/74 func (Implementation) SetStatusInCluster( ctx context.Context, req *operator.SetStatusInClusterRequest, diff --git a/plugins/wal-replica/internal/operator/validation.go b/plugins/wal-replica/internal/operator/validation.go index 42a0ae02..82e0ac00 100644 --- a/plugins/wal-replica/internal/operator/validation.go +++ b/plugins/wal-replica/internal/operator/validation.go @@ -69,8 +69,8 @@ func (Implementation) ValidateClusterChange( metadata.PluginName, ) - newConfiguration := config.FromParameters(newClusterHelper) - oldConfiguration := config.FromParameters(oldClusterHelper) + newConfiguration, _ := config.FromParameters(newClusterHelper) + oldConfiguration, _ := config.FromParameters(oldClusterHelper) result.ValidationErrors = config.ValidateChanges(oldConfiguration, newConfiguration, newClusterHelper) return result, nil diff --git a/plugins/wal-replica/internal/reconciler/replica.go b/plugins/wal-replica/internal/reconciler/replica.go index a448f044..49858842 100644 --- a/plugins/wal-replica/internal/reconciler/replica.go +++ b/plugins/wal-replica/internal/reconciler/replica.go @@ -4,234 +4,278 @@ package reconciler import ( - "context" - "fmt" - "strings" - - cnpgv1 "github.com/cloudnative-pg/api/pkg/api/v1" - "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/common" - "github.com/cloudnative-pg/machinery/pkg/log" - "github.com/documentdb/cnpg-i-wal-replica/internal/config" - "github.com/documentdb/cnpg-i-wal-replica/internal/k8sclient" - "github.com/documentdb/cnpg-i-wal-replica/pkg/metadata" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" +"context" +"fmt" +"strconv" +"strings" + +cnpgv1 "github.com/cloudnative-pg/api/pkg/api/v1" +"github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/common" +"github.com/cloudnative-pg/machinery/pkg/log" +"github.com/documentdb/cnpg-i-wal-replica/internal/config" +"github.com/documentdb/cnpg-i-wal-replica/internal/k8sclient" +"github.com/documentdb/cnpg-i-wal-replica/pkg/metadata" +appsv1 "k8s.io/api/apps/v1" +corev1 "k8s.io/api/core/v1" +"k8s.io/apimachinery/pkg/api/errors" +"k8s.io/apimachinery/pkg/api/resource" +metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +"k8s.io/apimachinery/pkg/types" +"sigs.k8s.io/controller-runtime/pkg/client" ) +// CreateWalReplica ensures a WAL receiver Deployment and PVC exist for the given cluster. func CreateWalReplica( - ctx context.Context, - cluster *cnpgv1.Cluster, +ctx context.Context, +cluster *cnpgv1.Cluster, ) error { - logger := log.FromContext(ctx).WithName("CreateWalReplica") - - // Build Deployment name unique per cluster - deploymentName := fmt.Sprintf("%s-wal-receiver", cluster.Name) - namespace := cluster.Namespace - client := k8sclient.MustGet() - - helper := common.NewPlugin( - *cluster, - metadata.PluginName, - ) - - configuration := config.FromParameters(helper) - - // TODO remove this once the operator functions are fixed - configuration.ApplyDefaults(cluster) - - // Needs a PVC to store the wal data - existingPVC := &corev1.PersistentVolumeClaim{} - err := client.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: namespace}, existingPVC) - if err != nil && errors.IsNotFound(err) { - logger.Info("WAL replica PVC not found. Creating a new WAL replica PVC") - - walReplicaPVC := &corev1.PersistentVolumeClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: deploymentName, - Namespace: namespace, - Labels: map[string]string{ - "app": deploymentName, - "cnpg.io/cluster": cluster.Name, - }, - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: cluster.APIVersion, - Kind: cluster.Kind, - Name: cluster.Name, - UID: cluster.UID, - }, - }, - }, - Spec: corev1.PersistentVolumeClaimSpec{ - AccessModes: []corev1.PersistentVolumeAccessMode{ - corev1.ReadWriteOnce, - }, - Resources: corev1.VolumeResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceStorage: resource.MustParse("10Gi"), - }, - }, - }, - } - - err = client.Create(ctx, walReplicaPVC) - if err != nil { - return err - } - } else if err != nil { - return err - } - - walDir := configuration.WalDirectory - - // Put the strings together so they run as separate commands, then rewrap - // them in a single arg - args := []string{ - strings.Join([]string{ - GetCommandForWalReceiver(configuration, walDir, true), - "&&", - GetCommandForWalReceiver(configuration, walDir, false), - }, " "), - } - - // Create or patch Deployment - existing := &appsv1.Deployment{} - err = client.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: namespace}, existing) - if err != nil { - dep := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: deploymentName, - Namespace: namespace, - Labels: map[string]string{ - "app": deploymentName, - "cnpg.io/cluster": cluster.Name, - }, - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: cluster.APIVersion, - Kind: cluster.Kind, - Name: cluster.Name, - UID: cluster.UID, - }, - }, - }, - Spec: appsv1.DeploymentSpec{ - Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": deploymentName}}, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": deploymentName}}, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{{ - Name: "wal-receiver", - Image: configuration.Image, - Command: []string{"/bin/bash", "-c"}, - Args: args, - VolumeMounts: []corev1.VolumeMount{ - { - Name: deploymentName, - MountPath: walDir, - }, - { - Name: "ca", - MountPath: "/var/lib/postgresql/rootcert", - ReadOnly: true, - }, - { - Name: "tls", - MountPath: "/var/lib/postgresql/cert", - ReadOnly: true, - }, - }, - }}, - Volumes: []corev1.Volume{ - { - Name: deploymentName, - VolumeSource: corev1.VolumeSource{ - PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: deploymentName, - }, - }, - }, - { - Name: "ca", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: cluster.Status.Certificates.ServerCASecret, - DefaultMode: int32Ptr(0600), - }, - }, - }, - { - Name: "tls", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: cluster.Status.Certificates.ReplicationTLSSecret, - DefaultMode: int32Ptr(0600), - }, - }, - }, - }, - SecurityContext: &corev1.PodSecurityContext{ - RunAsUser: int64Ptr(105), - RunAsGroup: int64Ptr(103), - FSGroup: int64Ptr(103), - }, - RestartPolicy: corev1.RestartPolicyAlways, - }, - }, - }, - } - if createErr := client.Create(ctx, dep); createErr != nil { - logger.Error(createErr, "creating wal receiver deployment") - return createErr - } - logger.Info("created wal receiver deployment", "name", deploymentName) - } else { - // TODO handle patch - } - - return nil -} - -// TODO change this to just use a custom image that creates the slot and the replica -func GetCommandForWalReceiver(configuration *config.Configuration, walDir string, createSlot bool) string { - connectionString := fmt.Sprintf("postgres://%s@%s/postgres?sslmode=verify-full&sslrootcert=%s&sslcert=%s&sslkey=%s", - "streaming_replica", // user - configuration.ReplicationHost, - "/var/lib/postgresql/rootcert/ca.crt", // root cert - "/var/lib/postgresql/cert/tls.crt", // cert - "/var/lib/postgresql/cert/tls.key") // key - createSlotFlag := "" - if createSlot { - createSlotFlag = "--create-slot --if-not-exists" - } - - // TODO have a real check here - verboseFlag := "" - if true { - verboseFlag = "--verbose" - } - - synchronousFlag := "" - if configuration.Synchronous == config.SynchronousActive { - synchronousFlag = "--synchronous" - } - - return fmt.Sprintf("pg_receivewal --slot wal_replica --compress 0 --directory %s --dbname \"%s\" %s %s %s", - walDir, - connectionString, - createSlotFlag, - verboseFlag, - synchronousFlag, - ) +logger := log.FromContext(ctx).WithName("CreateWalReplica") + +deploymentName := fmt.Sprintf("%s-wal-receiver", cluster.Name) +namespace := cluster.Namespace +k8sClient := k8sclient.MustGet() + +helper := common.NewPlugin(*cluster, metadata.PluginName) +configuration, valErrs := config.FromParameters(helper) +if len(valErrs) > 0 { +return fmt.Errorf("invalid plugin configuration: %s", valErrs[0].Message) +} + +// TODO: remove ApplyDefaults once MutateCluster is implemented upstream +configuration.ApplyDefaults(cluster) + +ownerRef := buildOwnerReference(cluster) + +if err := ensurePVC(ctx, k8sClient, deploymentName, namespace, cluster.Name, configuration, ownerRef); err != nil { +return err +} + +desiredDep := buildDeployment(deploymentName, namespace, cluster, configuration, ownerRef) + +existing := &appsv1.Deployment{} +err := k8sClient.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: namespace}, existing) +if err != nil { +if !errors.IsNotFound(err) { +return err +} +if createErr := k8sClient.Create(ctx, desiredDep); createErr != nil { +logger.Error(createErr, "creating wal receiver deployment") +return createErr +} +logger.Info("created wal receiver deployment", "name", deploymentName) +return nil +} + +// Patch existing Deployment with desired spec +patch := client.MergeFrom(existing.DeepCopy()) +existing.Spec.Template.Spec.Containers = desiredDep.Spec.Template.Spec.Containers +existing.Spec.Template.Spec.Volumes = desiredDep.Spec.Template.Spec.Volumes +existing.Spec.Template.Spec.SecurityContext = desiredDep.Spec.Template.Spec.SecurityContext +if err := k8sClient.Patch(ctx, existing, patch); err != nil { +logger.Error(err, "patching wal receiver deployment") +return err +} +logger.Info("patched wal receiver deployment", "name", deploymentName) + +return nil +} + +func buildOwnerReference(cluster *cnpgv1.Cluster) metav1.OwnerReference { +return metav1.OwnerReference{ +APIVersion: cluster.APIVersion, +Kind: cluster.Kind, +Name: cluster.Name, +UID: cluster.UID, +Controller: boolPtr(true), +BlockOwnerDeletion: boolPtr(true), +} +} + +func ensurePVC( +ctx context.Context, +k8sClient client.Client, +name, namespace, clusterName string, +cfg *config.Configuration, +ownerRef metav1.OwnerReference, +) error { +logger := log.FromContext(ctx).WithName("ensurePVC") + +existing := &corev1.PersistentVolumeClaim{} +err := k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, existing) +if err == nil { +return nil +} +if !errors.IsNotFound(err) { +return err +} + +logger.Info("creating WAL replica PVC", "name", name, "size", cfg.WalPVCSize) +pvc := &corev1.PersistentVolumeClaim{ +ObjectMeta: metav1.ObjectMeta{ +Name: name, +Namespace: namespace, +Labels: map[string]string{ +"app": name, +"cnpg.io/cluster": clusterName, +}, +OwnerReferences: []metav1.OwnerReference{ownerRef}, +}, +Spec: corev1.PersistentVolumeClaimSpec{ +AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, +Resources: corev1.VolumeResourceRequirements{ +Requests: corev1.ResourceList{ +corev1.ResourceStorage: resource.MustParse(cfg.WalPVCSize), +}, +}, +}, +} + +return k8sClient.Create(ctx, pvc) +} + +func buildDeployment( +name, namespace string, +cluster *cnpgv1.Cluster, +cfg *config.Configuration, +ownerRef metav1.OwnerReference, +) *appsv1.Deployment { +walDir := cfg.WalDirectory + +args := []string{ +strings.Join([]string{ +buildWalReceiverCommand(cfg, walDir, true), +"&&", +buildWalReceiverCommand(cfg, walDir, false), +}, " "), +} + +container := corev1.Container{ +Name: "wal-receiver", +Image: cfg.Image, +Command: []string{"/bin/bash", "-c"}, +Args: args, +VolumeMounts: []corev1.VolumeMount{ +{Name: name, MountPath: walDir}, +{Name: "ca", MountPath: "/var/lib/postgresql/rootcert", ReadOnly: true}, +{Name: "tls", MountPath: "/var/lib/postgresql/cert", ReadOnly: true}, +}, +LivenessProbe: &corev1.Probe{ +ProbeHandler: corev1.ProbeHandler{ +Exec: &corev1.ExecAction{ +Command: []string{"pgrep", "-f", "pg_receivewal"}, +}, +}, +InitialDelaySeconds: 10, +PeriodSeconds: 30, +FailureThreshold: 3, +}, +ReadinessProbe: &corev1.Probe{ +ProbeHandler: corev1.ProbeHandler{ +Exec: &corev1.ExecAction{ +Command: []string{"pgrep", "-f", "pg_receivewal"}, +}, +}, +InitialDelaySeconds: 5, +PeriodSeconds: 10, +}, +} + +return &appsv1.Deployment{ +ObjectMeta: metav1.ObjectMeta{ +Name: name, +Namespace: namespace, +Labels: map[string]string{ +"app": name, +"cnpg.io/cluster": cluster.Name, +}, +OwnerReferences: []metav1.OwnerReference{ownerRef}, +}, +Spec: appsv1.DeploymentSpec{ +Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": name}}, +Template: corev1.PodTemplateSpec{ +ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": name}}, +Spec: corev1.PodSpec{ +Containers: []corev1.Container{container}, +Volumes: []corev1.Volume{ +{ +Name: name, +VolumeSource: corev1.VolumeSource{ +PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ +ClaimName: name, +}, +}, +}, +{ +Name: "ca", +VolumeSource: corev1.VolumeSource{ +Secret: &corev1.SecretVolumeSource{ +SecretName: cluster.Status.Certificates.ServerCASecret, +DefaultMode: int32Ptr(0600), +}, +}, +}, +{ +Name: "tls", +VolumeSource: corev1.VolumeSource{ +Secret: &corev1.SecretVolumeSource{ +SecretName: cluster.Status.Certificates.ReplicationTLSSecret, +DefaultMode: int32Ptr(0600), +}, +}, +}, +}, +// PostgreSQL user/group IDs matching the CNPG base image +SecurityContext: &corev1.PodSecurityContext{ +RunAsUser: int64Ptr(105), +RunAsGroup: int64Ptr(103), +FSGroup: int64Ptr(103), +}, +RestartPolicy: corev1.RestartPolicyAlways, +}, +}, +}, +} +} + +func buildWalReceiverCommand(cfg *config.Configuration, walDir string, createSlot bool) string { +connectionString := fmt.Sprintf( +"postgres://%s@%s/postgres?sslmode=verify-full&sslrootcert=%s&sslcert=%s&sslkey=%s", +"streaming_replica", +cfg.ReplicationHost, +"/var/lib/postgresql/rootcert/ca.crt", +"/var/lib/postgresql/cert/tls.crt", +"/var/lib/postgresql/cert/tls.key", +) + +parts := []string{ +"pg_receivewal", +"--slot", "wal_replica", +"--compress", strconv.Itoa(cfg.Compression), +"--directory", walDir, +"--dbname", fmt.Sprintf("%q", connectionString), +} + +if createSlot { +parts = append(parts, "--create-slot", "--if-not-exists") +} +if cfg.Verbose { +parts = append(parts, "--verbose") +} +if cfg.Synchronous == config.SynchronousActive { +parts = append(parts, "--synchronous") +} + +return strings.Join(parts, " ") +} + +func boolPtr(b bool) *bool { +return &b } func int64Ptr(i int64) *int64 { - return &i +return &i } + func int32Ptr(i int32) *int32 { - return &i +return &i } diff --git a/plugins/wal-replica/internal/reconciler/replica_test.go b/plugins/wal-replica/internal/reconciler/replica_test.go new file mode 100644 index 00000000..1df7a5f9 --- /dev/null +++ b/plugins/wal-replica/internal/reconciler/replica_test.go @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package reconciler + +import ( +"strings" +"testing" + +cnpgv1 "github.com/cloudnative-pg/api/pkg/api/v1" +metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +"github.com/documentdb/cnpg-i-wal-replica/internal/config" +) + +func TestBuildWalReceiverCommand_CreateSlot(t *testing.T) { +cfg := &config.Configuration{ +ReplicationHost: "test-cluster-rw", +Verbose: true, +Compression: 0, +Synchronous: config.SynchronousInactive, +} + +cmd := buildWalReceiverCommand(cfg, "/var/lib/postgresql/wal", true) + +if !strings.Contains(cmd, "pg_receivewal") { +t.Error("expected pg_receivewal in command") +} +if !strings.Contains(cmd, "--create-slot") { +t.Error("expected --create-slot flag") +} +if !strings.Contains(cmd, "--if-not-exists") { +t.Error("expected --if-not-exists flag") +} +if !strings.Contains(cmd, "--verbose") { +t.Error("expected --verbose flag") +} +if strings.Contains(cmd, "--synchronous") { +t.Error("did not expect --synchronous flag when inactive") +} +} + +func TestBuildWalReceiverCommand_NoCreateSlot(t *testing.T) { +cfg := &config.Configuration{ +ReplicationHost: "test-cluster-rw", +Verbose: false, +Compression: 5, +Synchronous: config.SynchronousActive, +} + +cmd := buildWalReceiverCommand(cfg, "/wal", false) + +if strings.Contains(cmd, "--create-slot") { +t.Error("should not have --create-slot when createSlot=false") +} +if strings.Contains(cmd, "--verbose") { +t.Error("should not have --verbose when verbose=false") +} +if !strings.Contains(cmd, "--synchronous") { +t.Error("expected --synchronous flag when active") +} +if !strings.Contains(cmd, "--compress 5") { +t.Error("expected --compress 5") +} +} + +func TestBuildDeployment_Structure(t *testing.T) { +cluster := &cnpgv1.Cluster{ +ObjectMeta: metav1.ObjectMeta{ +Name: "test-cluster", +Namespace: "default", +UID: "test-uid-123", +}, +TypeMeta: metav1.TypeMeta{ +APIVersion: "postgresql.cnpg.io/v1", +Kind: "Cluster", +}, +Status: cnpgv1.ClusterStatus{ +Certificates: cnpgv1.CertificatesStatus{ + CertificatesConfiguration: cnpgv1.CertificatesConfiguration{ +ServerCASecret: "test-cluster-ca", +ReplicationTLSSecret: "test-cluster-replication", + }, +}, +}, +} + +cfg := &config.Configuration{ +Image: "postgres:16", +ReplicationHost: "test-cluster-rw", +WalDirectory: "/var/lib/postgresql/wal", +WalPVCSize: "10Gi", +Verbose: true, +Compression: 0, +Synchronous: config.SynchronousInactive, +} + +ownerRef := buildOwnerReference(cluster) +dep := buildDeployment("test-cluster-wal-receiver", "default", cluster, cfg, ownerRef) + +if dep.Name != "test-cluster-wal-receiver" { +t.Errorf("expected deployment name test-cluster-wal-receiver, got %q", dep.Name) +} +if dep.Namespace != "default" { +t.Errorf("expected namespace default, got %q", dep.Namespace) +} +if len(dep.OwnerReferences) != 1 { +t.Fatalf("expected 1 owner reference, got %d", len(dep.OwnerReferences)) +} +if dep.OwnerReferences[0].Controller == nil || !*dep.OwnerReferences[0].Controller { +t.Error("expected Controller=true on OwnerReference") +} +if dep.OwnerReferences[0].BlockOwnerDeletion == nil || !*dep.OwnerReferences[0].BlockOwnerDeletion { +t.Error("expected BlockOwnerDeletion=true on OwnerReference") +} + +// Check container +containers := dep.Spec.Template.Spec.Containers +if len(containers) != 1 { +t.Fatalf("expected 1 container, got %d", len(containers)) +} +container := containers[0] + +if container.Name != "wal-receiver" { +t.Errorf("expected container name wal-receiver, got %q", container.Name) +} +if container.Image != "postgres:16" { +t.Errorf("expected image postgres:16, got %q", container.Image) +} +if container.LivenessProbe == nil { +t.Error("expected liveness probe to be set") +} +if container.ReadinessProbe == nil { +t.Error("expected readiness probe to be set") +} + +// Check volumes +volumes := dep.Spec.Template.Spec.Volumes +if len(volumes) != 3 { +t.Errorf("expected 3 volumes, got %d", len(volumes)) +} + +// Check security context +sc := dep.Spec.Template.Spec.SecurityContext +if sc == nil { +t.Fatal("expected security context") +} +if *sc.RunAsUser != 105 { +t.Errorf("expected RunAsUser=105, got %d", *sc.RunAsUser) +} +} + +func TestBuildOwnerReference(t *testing.T) { +cluster := &cnpgv1.Cluster{ +ObjectMeta: metav1.ObjectMeta{ +Name: "test", +UID: "uid-abc", +}, +TypeMeta: metav1.TypeMeta{ +APIVersion: "postgresql.cnpg.io/v1", +Kind: "Cluster", +}, +} + +ref := buildOwnerReference(cluster) + +if ref.Name != "test" { +t.Errorf("expected name test, got %q", ref.Name) +} +if ref.Controller == nil || !*ref.Controller { +t.Error("expected Controller=true") +} +if ref.BlockOwnerDeletion == nil || !*ref.BlockOwnerDeletion { +t.Error("expected BlockOwnerDeletion=true") +} +} From a8cb9d80472bc6a93b41486b098036b4c7abb6c6 Mon Sep 17 00:00:00 2001 From: German Date: Wed, 11 Mar 2026 08:46:15 -0700 Subject: [PATCH 12/12] fix: WAL replica health probes and PVC owner reference - Replace pgrep with /proc-based process detection in liveness/readiness probes, since pgrep is not available in the CNPG PostgreSQL image - Use non-controller OwnerReference for the WAL receiver PVC so CNPG does not discover it as a managed PVC (which requires cnpg.io/pvcRole label), fixing the 'unknown pvc role name' reconciler error - Add test for buildNonControllerOwnerReference Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../internal/reconciler/replica.go | 21 +++++++++++++--- .../internal/reconciler/replica_test.go | 25 +++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/plugins/wal-replica/internal/reconciler/replica.go b/plugins/wal-replica/internal/reconciler/replica.go index 49858842..8c2e9987 100644 --- a/plugins/wal-replica/internal/reconciler/replica.go +++ b/plugins/wal-replica/internal/reconciler/replica.go @@ -45,8 +45,12 @@ return fmt.Errorf("invalid plugin configuration: %s", valErrs[0].Message) configuration.ApplyDefaults(cluster) ownerRef := buildOwnerReference(cluster) +// Use a non-controller OwnerReference for the PVC so CNPG does not +// discover it as a managed PVC (which would require a cnpg.io/pvcRole label). +// Garbage collection still works; only the "controller" flag is cleared. +pvcOwnerRef := buildNonControllerOwnerReference(cluster) -if err := ensurePVC(ctx, k8sClient, deploymentName, namespace, cluster.Name, configuration, ownerRef); err != nil { +if err := ensurePVC(ctx, k8sClient, deploymentName, namespace, cluster.Name, configuration, pvcOwnerRef); err != nil { return err } @@ -91,6 +95,17 @@ BlockOwnerDeletion: boolPtr(true), } } +func buildNonControllerOwnerReference(cluster *cnpgv1.Cluster) metav1.OwnerReference { +return metav1.OwnerReference{ +APIVersion: cluster.APIVersion, +Kind: cluster.Kind, +Name: cluster.Name, +UID: cluster.UID, +Controller: boolPtr(false), +BlockOwnerDeletion: boolPtr(true), +} +} + func ensurePVC( ctx context.Context, k8sClient client.Client, @@ -162,7 +177,7 @@ VolumeMounts: []corev1.VolumeMount{ LivenessProbe: &corev1.Probe{ ProbeHandler: corev1.ProbeHandler{ Exec: &corev1.ExecAction{ -Command: []string{"pgrep", "-f", "pg_receivewal"}, +Command: []string{"sh", "-c", "cat /proc/*/cmdline 2>/dev/null | tr '\\0' ' ' | grep -q pg_receivewal"}, }, }, InitialDelaySeconds: 10, @@ -172,7 +187,7 @@ FailureThreshold: 3, ReadinessProbe: &corev1.Probe{ ProbeHandler: corev1.ProbeHandler{ Exec: &corev1.ExecAction{ -Command: []string{"pgrep", "-f", "pg_receivewal"}, +Command: []string{"sh", "-c", "cat /proc/*/cmdline 2>/dev/null | tr '\\0' ' ' | grep -q pg_receivewal"}, }, }, InitialDelaySeconds: 5, diff --git a/plugins/wal-replica/internal/reconciler/replica_test.go b/plugins/wal-replica/internal/reconciler/replica_test.go index 1df7a5f9..e0d54728 100644 --- a/plugins/wal-replica/internal/reconciler/replica_test.go +++ b/plugins/wal-replica/internal/reconciler/replica_test.go @@ -174,3 +174,28 @@ if ref.BlockOwnerDeletion == nil || !*ref.BlockOwnerDeletion { t.Error("expected BlockOwnerDeletion=true") } } + +func TestBuildNonControllerOwnerReference(t *testing.T) { +cluster := &cnpgv1.Cluster{ +ObjectMeta: metav1.ObjectMeta{ +Name: "test", +UID: "uid-abc", +}, +TypeMeta: metav1.TypeMeta{ +APIVersion: "postgresql.cnpg.io/v1", +Kind: "Cluster", +}, +} + +ref := buildNonControllerOwnerReference(cluster) + +if ref.Name != "test" { +t.Errorf("expected name test, got %q", ref.Name) +} +if ref.Controller == nil || *ref.Controller { +t.Error("expected Controller=false") +} +if ref.BlockOwnerDeletion == nil || !*ref.BlockOwnerDeletion { +t.Error("expected BlockOwnerDeletion=true") +} +}