Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions plugins/wal-replica/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
bin/
dist/
.env
.vscode/
.idea/
.task/
manifest.yaml
20 changes: 20 additions & 0 deletions plugins/wal-replica/Dockerfile
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this what CNPG does? Or could we go distroless?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do distroless, cnpg uses gcr.io/distroless/static-debian12:nonroot

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"]
23 changes: 23 additions & 0 deletions plugins/wal-replica/LICENSE
Original file line number Diff line number Diff line change
@@ -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.
57 changes: 57 additions & 0 deletions plugins/wal-replica/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# WAL Receiver Pod Manager (CNPG-I Plugin)

This plugin adds an optional standalone WAL receiver (pg_receivewal) Pod/Deployment
alongside a [CloudNativePG](https://github.com/cloudnative-pg/cloudnative-pg/) Cluster.
It reconciles a Deployment named
`<cluster-name>-wal-receiver` that continuously streams WAL files from the primary
cluster using `pg_receivewal`, supporting synchronous mode.

## Parameters

Add the plugin in the Cluster spec (example):

```yaml
spec:
plugins:
- name: cnpg-i-wal-replica.documentdb.io
parameters:
enabled: "true"
image: "ghcr.io/cloudnative-pg/postgresql:16"
replicationUser: streaming_replica
replicationPasswordSecretName: cluster-replication
replicationPasswordSecretKey: password # optional (default: password)
synchronous: "true" # optional (default true)
walDirectory: /var/lib/wal # optional (default /var/lib/wal)
# replicationHost: override-host.example # optional
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| enabled | bool | false | Enable or disable the plugin |
| image | string | ghcr.io/cloudnative-pg/postgresql:16 | Image providing pg_receivewal |
| replicationHost | string | <cluster>-rw | Host to connect for streaming |
| replicationUser | string | streaming_replica | Replication user |
| replicationPasswordSecretName | string | (required when enabled) | Secret containing replication password |
| replicationPasswordSecretKey | string | password | Key in the secret |
| synchronous | bool | true | Add --synchronous flag to pg_receivewal |
| walDirectory | string | /var/lib/wal | Local directory to store WAL |

The Deployment exposes a metrics port (9187) and creates a Service with the same name.

## Build

```bash
go build -o bin/cnpg-i-wal-replica main.go
```

## Status

The plugin status reflects only whether it is enabled.

## Future Work

* Add PVC / volume configuration for WAL directory
* Expose resource requests/limits and security context
* Garbage collection / retention policy for archived WAL
* Liveness/readiness refinements

5 changes: 5 additions & 0 deletions plugins/wal-replica/cmd/plugin/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

// Package plugin implements the command to start the plugin
package plugin
39 changes: 39 additions & 0 deletions plugins/wal-replica/cmd/plugin/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package plugin

import (
"github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/http"
"github.com/cloudnative-pg/cnpg-i/pkg/operator"
"github.com/cloudnative-pg/cnpg-i/pkg/reconciler"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"

"github.com/documentdb/cnpg-i-wal-replica/internal/identity"
operatorImpl "github.com/documentdb/cnpg-i-wal-replica/internal/operator"
reconcilerImpl "github.com/documentdb/cnpg-i-wal-replica/internal/reconciler"
)

// NewCmd creates the `plugin` command
func NewCmd() *cobra.Command {
cmd := http.CreateMainCmd(identity.Implementation{}, func(server *grpc.Server) error {
// Register the declared implementations
operator.RegisterOperatorServer(server, operatorImpl.Implementation{})
reconciler.RegisterReconcilerHooksServer(server, reconcilerImpl.Implementation{})
return nil
})

// If you want to provide your own logr.Logger here, inject it into a context.Context
// with logr.NewContext(ctx, logger) and pass it to cmd.SetContext(ctx)
logger := zap.New(zap.UseDevMode(true))
log.SetLogger(logger)

// Additional custom behaviour can be added by wrapping cmd.PersistentPreRun or cmd.Run

cmd.Use = "plugin"

return cmd
}
180 changes: 180 additions & 0 deletions plugins/wal-replica/doc/development.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Plugin Development

This section of the documentation illustrates the CNPG-I capabilities used by
the wal-replica plugin, how the plugin implementation uses them, and how
developers can build and deploy the plugin.

## Concepts

### Identity

The Identity interface defines the features supported by the plugin and is the
only interface that must always be implemented.

This information is essential for the operator to discover the plugin's
capabilities during startup.

The Identity interface provides:

- A mechanism for plugins to report readiness probes. Readiness is a
prerequisite for receiving events, and plugins are expected to always report
the most accurate readiness data available.
- The capabilities reported by the plugin, which determine the subsequent calls
the plugin will receive.
- Metadata about the plugin.

[API reference](https://github.com/cloudnative-pg/cnpg-i/blob/main/proto/identity.proto)

### Capabilities

This plugin implements the Operator and the Lifecycle capabilities.

#### Operator

This feature enables the plugin to receive events about the cluster creation and
mutations, this is defined by the following

``` proto
// ValidateCreate improves the behavior of the validating webhook that
// is called on creation of the Cluster resources
rpc ValidateClusterCreate(OperatorValidateClusterCreateRequest) returns (OperatorValidateClusterCreateResult) {}

// ValidateClusterChange improves the behavior of the validating webhook of
// is called on updates of the Cluster resources
rpc ValidateClusterChange(OperatorValidateClusterChangeRequest) returns (OperatorValidateClusterChangeResult) {}

// MutateCluster fills in the defaults inside a Cluster resource
rpc MutateCluster(OperatorMutateClusterRequest) returns (OperatorMutateClusterResult) {}
```

This interface allows plugins to implement important features like:

1. validating the cluster manifest during the creation and mutations
(it is expected that the plugin validate the parameters assigned to their
configuration).

2. mutating the cluster object before it is submitted to kubernetes API server,
for example to set default values for the plugin parameters.

[API reference](https://github.com/cloudnative-pg/cnpg-i/blob/main/proto/operator.proto)

The wal-replica plugin is using this to validate used-defined parameters, and to
set default values for the labels and annotations applied by the plugin if not
specified by the user.

#### Lifecycle

This feature enables the plugin to receive events and create patches for
Kubernetes resources `before` they are submitted to the API server.

To use this feature, the plugin must specify the resource and operation it wants
to be notified of.

Some examples of what it can be achieved through the lifecycle:

- add volume, volume mounts, sidecar containers, labels, annotations to pods,
especially necessary when implementing custom backup solutions
- modify any resource with some annotations or labels
- add/remove finalizers

[API reference](https://github.com/cloudnative-pg/cnpg-i/blob/main/proto/operator_lifecycle.proto):

The wal-replica plugin is using this to add labels, annotations and a sidecar
to the pods.

## Implementation

### Identity

1. Define a struct inside the `internal/identity` package that implements
the `pluginhelper.IdentityServer` interface.

2. Implement the following methods:

- `GetPluginMetadata`: return human-readable information about the plugin.
- `GetPluginCapabilities`: specify the features supported by the plugin. In
the wal-replica example, the
`PluginCapability_Service_TYPE_LIFECYCLE_SERVICE` is defined in the
corresponding Go [file](../internal/lifecycle/lifecycle.go).
- `Probe`: indicate whether the plugin is ready to serve requests; this
example is stateless, so it will always be ready.

### Lifecycle

This example implements the lifecycle service capabilities to add labels and
annotations to the pods. The `OperatorLifecycleServer` interface is implemented
inside the `internal/lifecycle` package.

The `OperatorLifecycleServer` interface requires several methods:

- `GetCapabilities`: describe the resources and operations the plugin should be
notified for

- `LifecycleHook`: is invoked for every operation against the Kubernetes API
server that matches the specifications returned by `GetCapabilities`

In this function, the plugin is expected to do pattern matching using
the `Kind` and the operation `Type` and proceed with the proper logic.

### Operator

The operator interface offers a way for the plugin to interact with the Cluster
resource webhooks.

Do that, the plugin should implement
the [operator](https://github.com/cloudnative-pg/cnpg-i/blob/main/proto/operator.proto)
interface, specifically the `MutateCluster`, `ValidateClusterCreate`,
and `ValidateClusterChange` rpc calls.

- `MutateCluster`: enriches the plugin defaulting webhook

- `ValidateClusterCreate` and `ValidateClusterChange`: enriches the plugin
validation logic.

The package `internal/operator` implements this interface.

### Startup Command

The plugin runs in its own pod, and its main command is implemented in
the `main.go` file.

This function uses the plugin helper library to create a GRPC server and manage
TLS.

Plugin developers are expected to use the `pluginhelper.CreateMainCmd`
to implement the `main` function, passing an implemented `Identity`
struct.

Further implementations can be registered within the callback function.

In the example we propose, that's done for **operator** and for the
**lifecycle** services in [file](../cmd/plugin/plugin.go):

``` proto
operator.RegisterOperatorServer(server, operatorImpl.Implementation{})
lifecycle.RegisterOperatorLifecycleServer(server, lifecycleImpl.Implementation{})
```

## Build and deploy the plugin

Users can test their own changes to the plugin by building a container image
running it inside a Kubernetes cluster with CloudNativePG and cert-manager
installed.

### Local build

The repository provides a [`Taskfile`](https://taskfile.dev/) that contains
several helpful commands to test the plugin in
a [CNPG development environment](https://github.com/cloudnative-pg/cloudnative-pg/tree/main/contribute/e2e_testing_environment#the-local-kubernetes-cluster-for-testing).

By executing `task local-kind-deploy`, a container image containing the
executable of the repository will be built and loaded inside the kind cluster.

Having done that, the wal-replica plugin deployment will be applied.

### CI/CD build

The repository provides a GitHub Actions workflow that, on pushes, builds a
container image and generates a manifest file that can be used to deploy the
plugin. The manifest is attached to the workflow run as an artifact, and can be
applied to the cluster.
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: cluster-example
spec:
instances: 3

plugins:
- name: cnpg-i-wal-replica.documentdb.io
parameters:
labels: |
{
"first-label": "first-label-value",
"second-label": "second-label-value"
}
annotations: |
{
"first-annotation": "first-annotation-value",
this is a mistake
}

storage:
size: 1Gi
16 changes: 16 additions & 0 deletions plugins/wal-replica/doc/examples/cluster-example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: cluster-example
spec:
instances: 1

plugins:
- name: cnpg-i-wal-replica.documentdb.io
parameters:
replicationHost: cluster-example-rw

storage:
size: 1Gi

logLevel: "debug"
Loading
Loading