Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions test/e2e/e2e_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
_ "github.com/operasoftware/cnpg-plugin-pgbackrest/test/e2e/internal/tests/backup"
_ "github.com/operasoftware/cnpg-plugin-pgbackrest/test/e2e/internal/tests/parallelarchive"
_ "github.com/operasoftware/cnpg-plugin-pgbackrest/test/e2e/internal/tests/replicacluster"
_ "github.com/operasoftware/cnpg-plugin-pgbackrest/test/e2e/internal/tests/walarchive"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand Down
25 changes: 10 additions & 15 deletions test/e2e/internal/logs/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,29 +84,24 @@ func GetPodContainerLogs(
// FindArchiveBatches finds "WAL archive batch prepared" log entries and returns the parsed data.
// Each returned map contains the structured log fields.
func FindArchiveBatches(logEntries []map[string]any) []map[string]any {
var batches []map[string]any

for _, logEntry := range logEntries {
// Check if this is a "WAL archive batch prepared" message
if msg, ok := logEntry["msg"].(string); ok && msg == "WAL archive batch prepared" {
batches = append(batches, logEntry)
}
}

return batches
return FindLogEntriesByMessage(logEntries, "WAL archive batch prepared")
}

// FindArchiveBatchCompletions finds "WAL archive batch completed" log entries and returns the parsed data.
// Each returned map contains the structured log fields.
func FindArchiveBatchCompletions(logEntries []map[string]any) []map[string]any {
var batches []map[string]any
return FindLogEntriesByMessage(logEntries, "WAL archive batch completed")
}

// FindLogEntriesByMessage returns the log entries whose "msg" field equals the given message.
func FindLogEntriesByMessage(logEntries []map[string]any, message string) []map[string]any {
var matches []map[string]any

for _, logEntry := range logEntries {
// Check if this is a "WAL archive batch completed" message
if msg, ok := logEntry["msg"].(string); ok && msg == "WAL archive batch completed" {
batches = append(batches, logEntry)
if msg, ok := logEntry["msg"].(string); ok && msg == message {
matches = append(matches, logEntry)
}
}

return batches
return matches
}
20 changes: 20 additions & 0 deletions test/e2e/internal/tests/walarchive/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
Copyright 2025, Opera Norway AS

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Package walarchive contains e2e tests verifying that WAL archiving works
// without taking a pgbackrest backup first, by creating the stanza lazily on
// the first WAL archive.
package walarchive
172 changes: 172 additions & 0 deletions test/e2e/internal/tests/walarchive/fixtures.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
Copyright 2025, Opera Norway AS

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package walarchive

import (
cloudnativepgv1 "github.com/cloudnative-pg/api/pkg/api/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

pluginPgbackrestV1 "github.com/operasoftware/cnpg-plugin-pgbackrest/api/v1"
"github.com/operasoftware/cnpg-plugin-pgbackrest/test/e2e/internal/objectstore"
)

const (
minio = "minio"
// size is the size of the PVCs for the object store and the cluster instances.
size = "1Gi"
pluginName = "pgbackrest.cnpg.opera.com"
srcClusterName = "wal-archive-source"
archiveName = "wal-archive"
backupName = "wal-archive-backup"
restoreClusterName = "wal-archive-restore"
)

// walArchiveTestResources contains the resources needed to test WAL archiving
// without a prior backup, plus backup and restore to prove those paths still work.
type walArchiveTestResources struct {
ObjectStoreResources *objectstore.Resources
Archive *pluginPgbackrestV1.Archive
Cluster *cloudnativepgv1.Cluster
Backup *cloudnativepgv1.Backup
RestoreCluster *cloudnativepgv1.Cluster
}

// createWalArchiveTestResources builds all resources for the WAL archiving test.
func createWalArchiveTestResources(namespace string) walArchiveTestResources {
return walArchiveTestResources{
ObjectStoreResources: objectstore.NewMinioObjectStoreResources(namespace, minio),
// maxParallel=1 so every WAL is archived in its own batch, which keeps the
// assertions on batch completions unambiguous.
Archive: objectstore.NewMinioArchive(namespace, archiveName, minio, 1),
Cluster: newClusterWithPlugin(namespace, srcClusterName),
Backup: newPluginBackup(namespace, backupName, srcClusterName),
RestoreCluster: newRestoreCluster(namespace, restoreClusterName),
}
}

// newClusterWithPlugin creates a cluster that only enables the plugin for WAL
// archiving. Crucially it defines no bootstrap and no backup, so the stanza must
// be created lazily on the first WAL archive.
func newClusterWithPlugin(namespace, name string) *cloudnativepgv1.Cluster {
return &cloudnativepgv1.Cluster{
TypeMeta: metav1.TypeMeta{
Kind: "Cluster",
APIVersion: "postgresql.cnpg.io/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: cloudnativepgv1.ClusterSpec{
Instances: 2,
ImagePullPolicy: corev1.PullAlways,
Plugins: []cloudnativepgv1.PluginConfiguration{
{
Name: pluginName,
Parameters: map[string]string{
"pgbackrestObjectName": archiveName,
},
},
},
PostgresConfiguration: cloudnativepgv1.PostgresConfiguration{
Parameters: map[string]string{
"log_min_messages": "DEBUG4",
},
},
StorageConfiguration: cloudnativepgv1.StorageConfiguration{
Size: size,
},
},
}
}

// newPluginBackup creates a plugin backup targeting the primary of the given cluster.
func newPluginBackup(namespace, name, clusterName string) *cloudnativepgv1.Backup {
return &cloudnativepgv1.Backup{
TypeMeta: metav1.TypeMeta{
Kind: "Backup",
APIVersion: "postgresql.cnpg.io/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: cloudnativepgv1.BackupSpec{
Cluster: cloudnativepgv1.LocalObjectReference{
Name: clusterName,
},
Method: "plugin",
Target: "primary",
PluginConfiguration: &cloudnativepgv1.BackupPluginConfiguration{
Name: pluginName,
},
},
}
}

// newRestoreCluster creates a cluster that bootstraps by recovering from the
// source cluster's archive, while also archiving its own WALs to the same store.
func newRestoreCluster(namespace, name string) *cloudnativepgv1.Cluster {
return &cloudnativepgv1.Cluster{
TypeMeta: metav1.TypeMeta{
Kind: "Cluster",
APIVersion: "postgresql.cnpg.io/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: cloudnativepgv1.ClusterSpec{
Instances: 2,
ImagePullPolicy: corev1.PullAlways,
Bootstrap: &cloudnativepgv1.BootstrapConfiguration{
Recovery: &cloudnativepgv1.BootstrapRecovery{
Source: "source",
},
},
Plugins: []cloudnativepgv1.PluginConfiguration{
{
Name: pluginName,
Parameters: map[string]string{
"pgbackrestObjectName": archiveName,
},
},
},
PostgresConfiguration: cloudnativepgv1.PostgresConfiguration{
Parameters: map[string]string{
"log_min_messages": "DEBUG4",
},
},
ExternalClusters: []cloudnativepgv1.ExternalCluster{
{
Name: "source",
PluginConfiguration: &cloudnativepgv1.PluginConfiguration{
Name: pluginName,
Parameters: map[string]string{
"pgbackrestObjectName": archiveName,
"stanza": srcClusterName,
},
},
},
},
StorageConfiguration: cloudnativepgv1.StorageConfiguration{
Size: size,
},
},
}
}
Loading