Skip to content

Commit 402c15e

Browse files
ermakov-olegAgalin
authored andcommitted
test(e2e): add point-in-time recovery (PITR) e2e test with backup plugin
Signed-off-by: ermakov-oleg <[email protected]>
1 parent 5f15de2 commit 402c15e

File tree

2 files changed

+266
-0
lines changed

2 files changed

+266
-0
lines changed

test/e2e/internal/tests/backup/backup_restore.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package backup
1919

2020
import (
2121
"fmt"
22+
"strings"
2223
"time"
2324

2425
v1 "github.com/cloudnative-pg/api/pkg/api/v1"
@@ -177,4 +178,201 @@ var _ = Describe("Backup and restore", func() {
177178
&s3BackupPluginBackupPluginRestore{},
178179
),
179180
)
181+
182+
DescribeTable("should perform point-in-time recovery",
183+
func(
184+
ctx SpecContext,
185+
factory pitrTestCaseFactory,
186+
) {
187+
testResources := factory.createBackupRestoreTestResources(namespace.Name)
188+
189+
By("starting the object store deployment")
190+
Expect(testResources.ObjectStoreResources.Create(ctx, cl)).To(Succeed())
191+
192+
By("creating the Archive")
193+
Expect(cl.Create(ctx, testResources.Archive)).To(Succeed())
194+
195+
By("creating a CloudNativePG cluster")
196+
src := testResources.SrcCluster
197+
Expect(cl.Create(ctx, testResources.SrcCluster)).To(Succeed())
198+
199+
By("having the cluster ready")
200+
Eventually(func(g Gomega) {
201+
g.Expect(cl.Get(
202+
ctx,
203+
types.NamespacedName{
204+
Name: src.Name,
205+
Namespace: src.Namespace,
206+
},
207+
src)).To(Succeed())
208+
g.Expect(internalCluster.IsReady(*src)).To(BeTrue())
209+
}).WithTimeout(10 * time.Minute).WithPolling(10 * time.Second).Should(Succeed())
210+
211+
By("adding initial data to PostgreSQL")
212+
clientSet, cfg, err := internalClient.NewClientSet()
213+
Expect(err).NotTo(HaveOccurred())
214+
_, _, err = command.ExecuteInContainer(ctx,
215+
*clientSet,
216+
cfg,
217+
command.ContainerLocator{
218+
NamespaceName: src.Namespace,
219+
PodName: fmt.Sprintf("%v-1", src.Name),
220+
ContainerName: "postgres",
221+
},
222+
nil,
223+
[]string{"psql", "-tAc", "CREATE TABLE pitr_test (id int, data text, created_at timestamp DEFAULT now());"})
224+
Expect(err).NotTo(HaveOccurred())
225+
226+
_, _, err = command.ExecuteInContainer(ctx,
227+
*clientSet,
228+
cfg,
229+
command.ContainerLocator{
230+
NamespaceName: src.Namespace,
231+
PodName: fmt.Sprintf("%v-1", src.Name),
232+
ContainerName: "postgres",
233+
},
234+
nil,
235+
[]string{"psql", "-tAc", "INSERT INTO pitr_test (id, data) VALUES (1, 'before_backup');"})
236+
Expect(err).NotTo(HaveOccurred())
237+
238+
By("creating a backup")
239+
backup := testResources.SrcBackup
240+
Expect(cl.Create(ctx, backup)).To(Succeed())
241+
242+
By("waiting for the backup to complete")
243+
Eventually(func(g Gomega) {
244+
g.Expect(cl.Get(ctx, types.NamespacedName{Name: backup.Name, Namespace: backup.Namespace},
245+
backup)).To(Succeed())
246+
g.Expect(backup.Status.Phase).To(BeEquivalentTo(v1.BackupPhaseCompleted))
247+
}).Within(2 * time.Minute).WithPolling(5 * time.Second).Should(Succeed())
248+
249+
_, _, err = command.ExecuteInContainer(ctx,
250+
*clientSet,
251+
cfg,
252+
command.ContainerLocator{
253+
NamespaceName: src.Namespace,
254+
PodName: fmt.Sprintf("%v-1", src.Name),
255+
ContainerName: "postgres",
256+
},
257+
nil,
258+
[]string{"psql", "-tAc", "INSERT INTO pitr_test (id, data) VALUES (2, 'after_backup');"})
259+
Expect(err).NotTo(HaveOccurred())
260+
261+
By("recording timestamp for PITR target after adding more data")
262+
time.Sleep(2 * time.Second) // Ensure timestamp difference
263+
264+
// Record a timestamp for PITR target
265+
output, _, err := command.ExecuteInContainer(ctx,
266+
*clientSet,
267+
cfg,
268+
command.ContainerLocator{
269+
NamespaceName: src.Namespace,
270+
PodName: fmt.Sprintf("%v-1", src.Name),
271+
ContainerName: "postgres",
272+
},
273+
nil,
274+
[]string{"psql", "-tAc", "SELECT now()::text;"})
275+
Expect(err).NotTo(HaveOccurred())
276+
pitrTargetTime := strings.TrimSpace(output)
277+
278+
By("adding final data that should not appear in PITR restore")
279+
_, _, err = command.ExecuteInContainer(ctx,
280+
*clientSet,
281+
cfg,
282+
command.ContainerLocator{
283+
NamespaceName: src.Namespace,
284+
PodName: fmt.Sprintf("%v-1", src.Name),
285+
ContainerName: "postgres",
286+
},
287+
nil,
288+
[]string{"psql", "-tAc", "INSERT INTO pitr_test (id, data) VALUES (3, 'should_not_appear_in_pitr');"})
289+
Expect(err).NotTo(HaveOccurred())
290+
291+
By("forcing WAL switch to ensure data is archived")
292+
_, _, err = command.ExecuteInContainer(ctx,
293+
*clientSet,
294+
cfg,
295+
command.ContainerLocator{
296+
NamespaceName: src.Namespace,
297+
PodName: fmt.Sprintf("%v-1", src.Name),
298+
ContainerName: "postgres",
299+
},
300+
nil,
301+
[]string{"psql", "-tAc", "SELECT pg_switch_wal();"})
302+
Expect(err).NotTo(HaveOccurred())
303+
304+
time.Sleep(5 * time.Second)
305+
306+
By("performing point-in-time recovery to specific timestamp")
307+
pitrCluster := factory.createPITRCluster(namespace.Name, pitrTargetTime)
308+
Expect(cl.Create(ctx, pitrCluster)).To(Succeed())
309+
310+
By("having the PITR cluster ready")
311+
Eventually(func(g Gomega) {
312+
g.Expect(cl.Get(ctx,
313+
types.NamespacedName{Name: pitrCluster.Name, Namespace: pitrCluster.Namespace},
314+
pitrCluster)).To(Succeed())
315+
g.Expect(internalCluster.IsReady(*pitrCluster)).To(BeTrue())
316+
}).WithTimeout(10 * time.Minute).WithPolling(10 * time.Second).Should(Succeed())
317+
318+
By("verifying PITR recovered to correct point in time")
319+
320+
output, _, err = command.ExecuteInContainer(ctx,
321+
*clientSet,
322+
cfg,
323+
command.ContainerLocator{
324+
NamespaceName: pitrCluster.Namespace,
325+
PodName: fmt.Sprintf("%v-1", pitrCluster.Name),
326+
ContainerName: "postgres",
327+
},
328+
nil,
329+
[]string{"psql", "-tAc", "SELECT COUNT(*) FROM pitr_test WHERE data = 'before_backup';"})
330+
Expect(err).NotTo(HaveOccurred())
331+
Expect(strings.TrimSpace(output)).To(Equal("1"), "Should have initial data from before backup")
332+
333+
output, _, err = command.ExecuteInContainer(ctx,
334+
*clientSet,
335+
cfg,
336+
command.ContainerLocator{
337+
NamespaceName: pitrCluster.Namespace,
338+
PodName: fmt.Sprintf("%v-1", pitrCluster.Name),
339+
ContainerName: "postgres",
340+
},
341+
nil,
342+
[]string{"psql", "-tAc", "SELECT COUNT(*) FROM pitr_test WHERE data = 'after_backup';"})
343+
Expect(err).NotTo(HaveOccurred())
344+
Expect(strings.TrimSpace(output)).To(Equal("1"), "Should have data added after backup")
345+
346+
output, _, err = command.ExecuteInContainer(ctx,
347+
*clientSet,
348+
cfg,
349+
command.ContainerLocator{
350+
NamespaceName: pitrCluster.Namespace,
351+
PodName: fmt.Sprintf("%v-1", pitrCluster.Name),
352+
ContainerName: "postgres",
353+
},
354+
nil,
355+
[]string{"psql", "-tAc", "SELECT COUNT(*) FROM pitr_test WHERE data = 'should_not_appear_in_pitr';"})
356+
Expect(err).NotTo(HaveOccurred())
357+
Expect(strings.TrimSpace(output)).To(Equal("0"), "Should NOT have data after PITR target time")
358+
359+
By("verifying total record count matches expected PITR state")
360+
output, _, err = command.ExecuteInContainer(ctx,
361+
*clientSet,
362+
cfg,
363+
command.ContainerLocator{
364+
NamespaceName: pitrCluster.Namespace,
365+
PodName: fmt.Sprintf("%v-1", pitrCluster.Name),
366+
ContainerName: "postgres",
367+
},
368+
nil,
369+
[]string{"psql", "-tAc", "SELECT COUNT(*) FROM pitr_test;"})
370+
Expect(err).NotTo(HaveOccurred())
371+
Expect(strings.TrimSpace(output)).To(Equal("2"), "Should have exactly 2 records in PITR restore (1 before backup + 1 after backup)")
372+
},
373+
Entry(
374+
"using TargetTime with plugin",
375+
&s3BackupPluginTargetTimeRestore{},
376+
),
377+
)
180378
})

test/e2e/internal/tests/backup/fixtures.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,18 @@ const (
3535
archiveName = "source"
3636
dstBackupName = "restore"
3737
restoreClusterName = "restore"
38+
pitrClusterName = "pitr-restore"
3839
)
3940

4041
type testCaseFactory interface {
4142
createBackupRestoreTestResources(namespace string) backupRestoreTestResources
4243
}
4344

45+
type pitrTestCaseFactory interface {
46+
testCaseFactory
47+
createPITRCluster(namespace string, targetTime string) *cloudnativepgv1.Cluster
48+
}
49+
4450
type backupRestoreTestResources struct {
4551
ObjectStoreResources *objectstore.Resources
4652
Archive *pluginPgbackrestV1.Archive
@@ -52,6 +58,10 @@ type backupRestoreTestResources struct {
5258

5359
type s3BackupPluginBackupPluginRestore struct{}
5460

61+
type s3BackupPluginTargetTimeRestore struct {
62+
s3BackupPluginBackupPluginRestore
63+
}
64+
5565
func (s s3BackupPluginBackupPluginRestore) createBackupRestoreTestResources(
5666
namespace string,
5767
) backupRestoreTestResources {
@@ -67,6 +77,64 @@ func (s s3BackupPluginBackupPluginRestore) createBackupRestoreTestResources(
6777
return result
6878
}
6979

80+
func (s s3BackupPluginTargetTimeRestore) createPITRCluster(
81+
namespace string,
82+
targetTime string,
83+
) *cloudnativepgv1.Cluster {
84+
cluster := &cloudnativepgv1.Cluster{
85+
TypeMeta: metav1.TypeMeta{
86+
Kind: "Cluster",
87+
APIVersion: "postgresql.cnpg.io/v1",
88+
},
89+
ObjectMeta: metav1.ObjectMeta{
90+
Name: pitrClusterName,
91+
Namespace: namespace,
92+
},
93+
Spec: cloudnativepgv1.ClusterSpec{
94+
Instances: 2,
95+
ImagePullPolicy: corev1.PullAlways,
96+
Bootstrap: &cloudnativepgv1.BootstrapConfiguration{
97+
Recovery: &cloudnativepgv1.BootstrapRecovery{
98+
Source: "source",
99+
RecoveryTarget: &cloudnativepgv1.RecoveryTarget{
100+
TargetTime: targetTime,
101+
},
102+
},
103+
},
104+
Plugins: []cloudnativepgv1.PluginConfiguration{
105+
{
106+
Name: "pgbackrest.cnpg.opera.com",
107+
Parameters: map[string]string{
108+
"pgbackrestObjectName": archiveName,
109+
},
110+
},
111+
},
112+
PostgresConfiguration: cloudnativepgv1.PostgresConfiguration{
113+
Parameters: map[string]string{
114+
"log_min_messages": "DEBUG4",
115+
},
116+
},
117+
ExternalClusters: []cloudnativepgv1.ExternalCluster{
118+
{
119+
Name: "source",
120+
PluginConfiguration: &cloudnativepgv1.PluginConfiguration{
121+
Name: "pgbackrest.cnpg.opera.com",
122+
Parameters: map[string]string{
123+
"pgbackrestObjectName": archiveName,
124+
"stanza": srcClusterName,
125+
},
126+
},
127+
},
128+
},
129+
StorageConfiguration: cloudnativepgv1.StorageConfiguration{
130+
Size: size,
131+
},
132+
},
133+
}
134+
135+
return cluster
136+
}
137+
70138
func newSrcPluginBackup(namespace string) *cloudnativepgv1.Backup {
71139
return &cloudnativepgv1.Backup{
72140
TypeMeta: metav1.TypeMeta{

0 commit comments

Comments
 (0)