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..1967f50b --- /dev/null +++ b/plugins/wal-replica/README.md @@ -0,0 +1,190 @@ +# WAL Replica Pod Manager (CNPG-I Plugin) + +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. + +## Features + +- **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: + 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 | 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 + +## 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 + +- 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 +``` + +### 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 + +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/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..b0dc4f97 --- /dev/null +++ b/plugins/wal-replica/cmd/plugin/plugin.go @@ -0,0 +1,35 @@ +// 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 + }) + + logger := zap.New(zap.UseDevMode(true)) + log.SetLogger(logger) + + cmd.Use = "receivewal" + + return cmd +} diff --git a/plugins/wal-replica/doc/development.md b/plugins/wal-replica/doc/development.md new file mode 100644 index 00000000..e8887f47 --- /dev/null +++ b/plugins/wal-replica/doc/development.md @@ -0,0 +1,442 @@ +# WAL Replica Plugin Development Guide + +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. + +## Overview + +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. + +### Plugin Metadata + +```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 + +### Identity Interface + +The Identity interface is fundamental to all CNPG-I plugins and defines: + +- **Plugin Metadata**: Name, version, description, and licensing information +- **Capabilities**: Which CNPG-I services the plugin implements +- **Readiness**: Health check mechanism for the plugin + +The identity implementation is located in [`internal/identity/impl.go`](../internal/identity/impl.go). + +**Key Methods:** +- `GetPluginMetadata()`: Returns plugin information from `pkg/metadata` +- `GetPluginCapabilities()`: Declares supported services (Operator + Reconciler) +- `Probe()`: Always returns ready (stateless plugin) + +[CNPG-I Identity API Reference](https://github.com/cloudnative-pg/cnpg-i/blob/main/proto/identity.proto) + +### Implemented Capabilities + +This plugin implements two core CNPG-I capabilities: + +#### 1. Operator Interface + +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) +``` + +**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 + +See [`internal/operator/validation.go`](../internal/operator/validation.go) and [`internal/operator/mutations.go`](../internal/operator/mutations.go). + +#### 2. Reconciler Hooks Interface + +Enables resource reconciliation and custom Kubernetes resource management: + +```go +// From internal/reconciler/ +rpc ReconcilerHook(ReconcilerHookRequest) returns (ReconcilerHookResponse) +``` + +**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 + +See [`internal/reconciler/replica.go`](../internal/reconciler/replica.go) for the main implementation. + +[CNPG-I Operator API Reference](https://github.com/cloudnative-pg/cnpg-i/blob/main/proto/operator.proto) + +## Architecture Deep Dive + +### Configuration Management + +The plugin uses a layered configuration approach: + +```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 +} +``` + +**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 +``` + +#### pg_receivewal Command Construction + +The plugin builds sophisticated `pg_receivewal` commands: + +```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&..." + +# 2. Continuous WAL streaming +pg_receivewal --slot wal_replica --compress 0 --directory /path/to/wal --dbname "postgres://..." [--synchronous] [--verbose] +``` + +### Security Implementation + +**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 + +**Pod Security:** +- Runs as PostgreSQL user (`uid: 105, gid: 103`) +- Proper filesystem permissions (`fsGroup: 103`) +- Read-only certificate mounts + +## Development Environment Setup + +### Prerequisites + +```bash +# Required tools +go 1.24.1+ +docker or podman +kubectl +kind (for local testing) + +# Required Kubernetes components +cloudnative-pg operator +cnpg-i framework +cert-manager (for TLS) +``` + +### Local Development + +#### 1. Environment Setup + +```bash +# Clone repository +git clone https://github.com/documentdb/cnpg-i-wal-replica +cd cnpg-i-wal-replica + +# Install dependencies +go mod download + +# Verify build +go build -o bin/cnpg-i-wal-replica main.go +``` + +#### 2. Code Structure Navigation + +``` +├── 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 +``` + +#### 3. Testing Locally + +```bash +# Build and test +./scripts/build.sh + +# Run with debugging +./scripts/run.sh + +# Test configuration parsing +go test ./internal/config/ + +# Test reconciliation logic +go test ./internal/reconciler/ +``` + +### 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 + +## Future Development Roadmap + +### Planned Enhancements + +- **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 + +### API Stability + +- Current API is alpha-level with potential breaking changes +- Plugin interface follows CNPG-I versioning conventions +- Configuration parameters may evolve based on user feedback + +## Resources + +- [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/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..c46aa637 --- /dev/null +++ b/plugins/wal-replica/doc/examples/cluster-example-with-mistake.yaml @@ -0,0 +1,14 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: cluster-example +spec: + instances: 3 + + plugins: + - name: cnpg-i-wal-replica.documentdb.io + parameters: + replication_host: "cluster-example-0.cluster-example-svc.cnpg-system.svc.cluster.local" + synchronous: "badvalue" + 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..7961a303 --- /dev/null +++ b/plugins/wal-replica/doc/examples/cluster-example.yaml @@ -0,0 +1,18 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: cluster-example +spec: + instances: 1 + + plugins: + - name: cnpg-i-wal-replica.documentdb.io + + replicationSlots: + synchronizeReplicas: + enabled: true + + storage: + size: 1Gi + + logLevel: "debug" diff --git a/plugins/wal-replica/go.mod b/plugins/wal-replica/go.mod new file mode 100644 index 00000000..59d3d087 --- /dev/null +++ b/plugins/wal-replica/go.mod @@ -0,0 +1,109 @@ +module github.com/documentdb/cnpg-i-wal-replica + +go 1.24.1 + +require ( + 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.3.3 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // 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.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.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.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.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.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.3-0.20250322232337-35a7c28c31ee // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pkg/errors v0.9.1 // 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.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 + 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.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-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 new file mode 100644 index 00000000..72330df6 --- /dev/null +++ b/plugins/wal-replica/go.sum @@ -0,0 +1,410 @@ +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/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= +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/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= +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/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= +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/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= +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/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= +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/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= +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/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= +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/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= +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/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= +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/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= +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 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= +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/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= +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= +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= +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/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 new file mode 100644 index 00000000..e9ee13a3 --- /dev/null +++ b/plugins/wal-replica/internal/config/config.go @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +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" +) + +// 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 +) + +// SynchronousMode represents the synchronous replication mode +type SynchronousMode string + +const ( + SynchronousUnset SynchronousMode = "" + SynchronousActive SynchronousMode = "active" + SynchronousInactive SynchronousMode = "inactive" +) + +const ( + defaultWalDir = "/var/lib/postgresql/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 + WalPVCSize 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] + cfg.WalPVCSize = helper.Parameters[WalPVCSize] + 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 + params[WalPVCSize] = c.WalPVCSize + 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 +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" + } +} 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..c0bb0288 --- /dev/null +++ b/plugins/wal-replica/internal/identity/impl.go @@ -0,0 +1,57 @@ +// 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{ + { // TODO find out why this does nothing + Type: &identity.PluginCapability_Service_{ + Service: &identity.PluginCapability_Service{ + Type: identity.PluginCapability_Service_TYPE_OPERATOR_SERVICE, + }, + }, + }, + { + 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..efd6d119 --- /dev/null +++ b/plugins/wal-replica/internal/operator/impl.go @@ -0,0 +1,56 @@ +// 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, + }, + }, + }, + /* 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: &operator.OperatorCapability_Rpc{ + Rpc: &operator.OperatorCapability_RPC{ + Type: operator.OperatorCapability_RPC_TYPE_MUTATE_CLUSTER, + }, + }, + }, + }, + }, 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..68417ecf --- /dev/null +++ b/plugins/wal-replica/internal/operator/mutations.go @@ -0,0 +1,61 @@ +// 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/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. +func (Implementation) MutateCluster( + 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 + } + + 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(cluster) + + mutatedCluster.Spec.Plugins[helper.PluginIndex].Parameters, err = config.ToParameters() + 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 + } + + 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..1629de99 --- /dev/null +++ b/plugins/wal-replica/internal/operator/status.go @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package operator + +import ( + "context" + "encoding/json" + "errors" + + 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" + "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 *cnpgv1.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") + } + + 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 { + logger.Error(err, "while unmarshalling plugin status", + "entry", pluginEntry) + return nil, err + } + } + + logger.Info("debug status snapshot", + "resourceVersion", cluster.ResourceVersion, + "pluginIndex", plg.PluginIndex, + "rawPluginStatus", pluginEntry.Status, + "decodedEnabled", status.Enabled, + ) + + if status.Enabled { + 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") + + 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..42a0ae02 --- /dev/null +++ b/plugins/wal-replica/internal/operator/validation.go @@ -0,0 +1,77 @@ +// 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/cloudnative-pg/machinery/pkg/log" + + "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( + 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 + } + + 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( + 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()) + 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/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/impl.go b/plugins/wal-replica/internal/reconciler/impl.go new file mode 100644 index 00000000..e94cd99a --- /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" + + cnpgv1 "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 := &cnpgv1.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..a448f044 --- /dev/null +++ b/plugins/wal-replica/internal/reconciler/replica.go @@ -0,0 +1,237 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +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" +) + +func CreateWalReplica( + 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, + ) +} + +func int64Ptr(i int64) *int64 { + return &i +} +func int32Ptr(i int32) *int32 { + return &i +} 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..99469564 --- /dev/null +++ b/plugins/wal-replica/kubernetes/deployment.yaml @@ -0,0 +1,44 @@ +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: + serviceAccountName: wal-replica-manager + containers: + - image: cnpg-i-wal-replica:latest + name: cnpg-i-wal-replica + ports: + - containerPort: 9090 + protocol: TCP + args: + - receivewal + - --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..b1f0287a --- /dev/null +++ b/plugins/wal-replica/kubernetes/kustomization.yaml @@ -0,0 +1,16 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: cnpg-system +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 + newName: ghcr.io/microsoft/documentdb-kubernetes-operator/wal-replica + newTag: latest 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/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-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 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..1670754a --- /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: "MIT", + 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..aeaa391e --- /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 -gcflags="all=-N -l" -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}\"}]}}}}"