Skip to content

Commit ed0b873

Browse files
jsilvelaarmrumnencia
authored
feat: enrich the Backup status with volume snapshots (cloudnative-pg#2838)
This patch enhances the status section of a volume snapshot Backup with Postgres metadata to match the status of an object store backup. Closes cloudnative-pg#2813 Signed-off-by: Jaime Silvela <jaime.silvela@enterprisedb.com> Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com> Signed-off-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com> Co-authored-by: Armando Ruocco <armando.ruocco@enterprisedb.com> Co-authored-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com>
1 parent 03eacc3 commit ed0b873

File tree

6 files changed

+142
-27
lines changed

6 files changed

+142
-27
lines changed

controllers/backup_controller.go

+29
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ import (
2525
storagesnapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
2626
corev1 "k8s.io/api/core/v1"
2727
apierrs "k8s.io/apimachinery/pkg/api/errors"
28+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2829
"k8s.io/apimachinery/pkg/runtime"
2930
"k8s.io/client-go/kubernetes"
3031
"k8s.io/client-go/tools/record"
3132
"k8s.io/client-go/util/retry"
33+
"k8s.io/utils/ptr"
3234
ctrl "sigs.k8s.io/controller-runtime"
3335
"sigs.k8s.io/controller-runtime/pkg/builder"
3436
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -322,6 +324,8 @@ func (r *BackupReconciler) startSnapshotBackup(
322324
backup.Status.SetAsStarted(targetPod, apiv1.BackupMethodVolumeSnapshot)
323325
// given that we use only kubernetes resources we can use the backup name as ID
324326
backup.Status.BackupID = backup.Name
327+
backup.Status.BackupName = backup.Name
328+
backup.Status.StartedAt = ptr.To(metav1.Now())
325329
if err := postgres.PatchBackupStatusAndRetry(ctx, r.Client, backup); err != nil {
326330
return nil, err
327331
}
@@ -375,10 +379,35 @@ func (r *BackupReconciler) startSnapshotBackup(
375379
}
376380

377381
backup.Status.BackupSnapshotStatus.SetSnapshotList(snapshots)
382+
if err := backupStatusFromSnapshots(snapshots, &backup.Status); err != nil {
383+
contextLogger.Error(err, "while enriching the backup status")
384+
}
378385

379386
return nil, postgres.PatchBackupStatusAndRetry(ctx, r.Client, backup)
380387
}
381388

389+
// backupStatusFromSnapshots adds fields to the backup status based on the snapshots
390+
func backupStatusFromSnapshots(
391+
snapshots volumesnapshot.Slice,
392+
backupStatus *apiv1.BackupStatus,
393+
) error {
394+
_, lastCreation := snapshots.GetSnapshotsInterval()
395+
backupStatus.StoppedAt = ptr.To(lastCreation)
396+
controldata, err := snapshots.GetControldata()
397+
if err != nil {
398+
return err
399+
}
400+
pairs := utils.ParsePgControldataOutput(controldata)
401+
402+
// the begin/end WAL and LSN are the same, since the instance was fenced
403+
// for the snapshot
404+
backupStatus.BeginWal = pairs["Latest checkpoint's REDO WAL file"]
405+
backupStatus.EndWal = pairs["Latest checkpoint's REDO WAL file"]
406+
backupStatus.BeginLSN = pairs["Latest checkpoint's REDO location"]
407+
backupStatus.EndLSN = pairs["Latest checkpoint's REDO location"]
408+
return nil
409+
}
410+
382411
// isErrorRetryable detects is an error is retryable or not
383412
func isErrorRetryable(err error) bool {
384413
return apierrs.IsServerTimeout(err) || apierrs.IsConflict(err) || apierrs.IsInternalError(err)

pkg/management/postgres/restore.go

+8-14
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
"os"
2626
"os/exec"
2727
"path"
28-
"regexp"
2928
"strings"
3029
"time"
3130

@@ -68,13 +67,12 @@ var (
6867
Steps: math.MaxInt32,
6968
}
7069

71-
enforcedParametersRegex = regexp.MustCompile(`(?P<PARAM>[a-z_]+) setting:\s+(?P<VALUE>[a-z0-9]+)`)
7270
pgControldataSettingsToParamsMap = map[string]string{
73-
"max_connections": "max_connections",
74-
"max_wal_senders": "max_wal_senders",
75-
"max_worker_processes": "max_worker_processes",
76-
"max_prepared_xacts": "max_prepared_transactions",
77-
"max_locks_per_xact": "max_locks_per_transaction",
71+
"max_connections setting": "max_connections",
72+
"max_wal_senders setting": "max_wal_senders",
73+
"max_worker_processes setting": "max_worker_processes",
74+
"max_prepared_xacts setting": "max_prepared_transactions",
75+
"max_locks_per_xact setting": "max_locks_per_transaction",
7876
}
7977
)
8078

@@ -641,13 +639,9 @@ func GetEnforcedParametersThroughPgControldata(pgData string) (map[string]string
641639
log.Debug("pg_controldata stdout", "stdout", stdoutBuffer.String())
642640

643641
enforcedParams := map[string]string{}
644-
for _, line := range strings.Split(stdoutBuffer.String(), "\n") {
645-
matches := enforcedParametersRegex.FindStringSubmatch(line)
646-
if len(matches) < 3 {
647-
continue
648-
}
649-
if param, ok := pgControldataSettingsToParamsMap[matches[1]]; ok {
650-
enforcedParams[param] = matches[2]
642+
for key, value := range utils.ParsePgControldataOutput(stdoutBuffer.String()) {
643+
if param, ok := pgControldataSettingsToParamsMap[key]; ok {
644+
enforcedParams[param] = value
651645
}
652646
}
653647
return enforcedParams, nil

pkg/reconciler/backup/volumesnapshot/reconciler.go

+24-11
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ func (se *Reconciler) ensurePodIsFenced(
174174
backup *apiv1.Backup,
175175
targetPodName string,
176176
) error {
177+
contextLogger := log.FromContext(ctx)
178+
177179
fencedInstances, err := utils.GetFencedInstances(cluster.Annotations)
178180
if err != nil {
179181
return fmt.Errorf("could not check if cluster is fenced: %v", err)
@@ -188,21 +190,27 @@ func (se *Reconciler) ensurePodIsFenced(
188190
return errors.New("cannot execute volume snapshot on a cluster that has fenced instances")
189191
}
190192

191-
// The list of fenced instances is empty, so we need to request
192-
// fencing for the target pod
193-
se.recorder.Eventf(backup, "Normal", "FencePod",
194-
"Requesting fencing for Pod %v", targetPodName)
195-
196-
if err := resources.ApplyFenceFunc(
193+
err = resources.ApplyFenceFunc(
197194
ctx,
198195
se.cli,
199196
cluster.Name,
200197
cluster.Namespace,
201198
targetPodName,
202199
utils.AddFencedInstance,
203-
); !errors.Is(err, utils.ErrorServerAlreadyFenced) {
200+
)
201+
if errors.Is(err, utils.ErrorServerAlreadyFenced) {
202+
return nil
203+
}
204+
if err != nil {
204205
return err
205206
}
207+
208+
// The list of fenced instances is empty, so we need to request
209+
// fencing for the target pod
210+
contextLogger.Info("Fencing Pod", "podName", targetPodName)
211+
se.recorder.Eventf(backup, "Normal", "FencePod",
212+
"Fencing Pod %v", targetPodName)
213+
206214
return nil
207215
}
208216

@@ -214,23 +222,28 @@ func (se *Reconciler) EnsurePodIsUnfenced(
214222
targetPod *corev1.Pod,
215223
) error {
216224
contextLogger := log.FromContext(ctx)
217-
contextLogger.Info("Unfencing Pod")
218225

219-
if err := resources.ApplyFenceFunc(
226+
err := resources.ApplyFenceFunc(
220227
ctx,
221228
se.cli,
222229
cluster.Name,
223230
cluster.Namespace,
224231
targetPod.Name,
225232
utils.RemoveFencedInstance,
226-
); err != nil {
233+
)
234+
if errors.Is(err, utils.ErrorServerAlreadyUnfenced) {
235+
return nil
236+
}
237+
if err != nil {
227238
return err
228239
}
229240

230241
// The list of fenced instances is empty, so we need to request
231242
// fencing for the target pod
243+
contextLogger.Info("Unfencing Pod", "podName", targetPod.Name)
232244
se.recorder.Eventf(backup, "Normal", "UnfencePod",
233-
"Un-fencing Pod %v", targetPod.Name)
245+
"Unfencing Pod %v", targetPod.Name)
246+
234247
return nil
235248
}
236249

pkg/reconciler/backup/volumesnapshot/resources.go

+39-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ package volumesnapshot
1919
import (
2020
"context"
2121
"errors"
22+
"fmt"
2223

2324
storagesnapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2426
"sigs.k8s.io/controller-runtime/pkg/client"
2527

2628
"github.com/cloudnative-pg/cloudnative-pg/pkg/utils"
@@ -57,20 +59,55 @@ type volumeSnapshotError struct {
5759

5860
// Error implements the error interface
5961
func (err volumeSnapshotError) Error() string {
60-
if err.InternalError.Message != nil {
62+
if err.InternalError.Message == nil {
6163
return "non specified volume snapshot error"
6264
}
6365
return *err.InternalError.Message
6466
}
6567

68+
// Slice represents a slice of []storagesnapshotv1.VolumeSnapshot
69+
type Slice []storagesnapshotv1.VolumeSnapshot
70+
71+
// GetSnapshotsInterval gets the earliest and latest creation times from a list of VolumeSnapshots
72+
func (s Slice) GetSnapshotsInterval() (metav1.Time, metav1.Time) {
73+
var firstCreation, lastCreation metav1.Time
74+
for idx := range s {
75+
volumeSnapshot := &s[idx]
76+
if firstCreation.IsZero() || lastCreation.IsZero() {
77+
firstCreation = volumeSnapshot.CreationTimestamp
78+
lastCreation = volumeSnapshot.CreationTimestamp
79+
continue
80+
}
81+
if volumeSnapshot.CreationTimestamp.Before(&firstCreation) {
82+
firstCreation = volumeSnapshot.CreationTimestamp
83+
}
84+
if lastCreation.Before(&volumeSnapshot.CreationTimestamp) {
85+
lastCreation = volumeSnapshot.CreationTimestamp
86+
}
87+
}
88+
return firstCreation, lastCreation
89+
}
90+
91+
// GetControldata retrieves the pg_controldata stored as an annotation in VolumeSnapshots
92+
func (s Slice) GetControldata() (string, error) {
93+
for _, volumeSnapshot := range s {
94+
pgControlData, ok := volumeSnapshot.Annotations[utils.PgControldataAnnotationName]
95+
if !ok {
96+
continue
97+
}
98+
return pgControlData, nil
99+
}
100+
return "", fmt.Errorf("could not retrieve pg_controldata from any snapshot")
101+
}
102+
66103
// GetBackupVolumeSnapshots extracts the list of volume snapshots related
67104
// to a backup name
68105
func GetBackupVolumeSnapshots(
69106
ctx context.Context,
70107
cli client.Client,
71108
namespace string,
72109
backupLabelName string,
73-
) ([]storagesnapshotv1.VolumeSnapshot, error) {
110+
) (Slice, error) {
74111
var list storagesnapshotv1.VolumeSnapshotList
75112

76113
if err := cli.List(

pkg/utils/parser.go

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
Copyright The CloudNativePG Contributors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package utils
18+
19+
import "strings"
20+
21+
// ParsePgControldataOutput parses a pg_controldata output into a map of key-value pairs
22+
func ParsePgControldataOutput(data string) map[string]string {
23+
pairs := make(map[string]string)
24+
lines := strings.Split(data, "\n")
25+
for _, line := range lines {
26+
frags := strings.Split(line, ":")
27+
if len(frags) != 2 {
28+
continue
29+
}
30+
pairs[strings.TrimSpace(frags[0])] = strings.TrimSpace(frags[1])
31+
}
32+
return pairs
33+
}

tests/e2e/volume_snapshot_test.go

+9
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,15 @@ var _ = Describe("Verify Volume Snapshot",
380380
AssertBackupConditionInClusterStatus(namespace, clusterToBackupName)
381381
})
382382

383+
By("checking that the backup status is correctly populated", func() {
384+
Expect(backup.Status.BeginWal).ToNot(BeEmpty())
385+
Expect(backup.Status.EndWal).ToNot(BeEmpty())
386+
Expect(backup.Status.BeginLSN).ToNot(BeEmpty())
387+
Expect(backup.Status.EndLSN).ToNot(BeEmpty())
388+
Expect(backup.Status.StoppedAt).ToNot(BeNil())
389+
Expect(backup.Status.StartedAt).ToNot(BeNil())
390+
})
391+
383392
var clusterToBackup *apiv1.Cluster
384393

385394
By("fetching the created cluster", func() {

0 commit comments

Comments
 (0)