@@ -19,6 +19,7 @@ package backup
1919
2020import (
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})
0 commit comments