Skip to content

Commit a5d575e

Browse files
authored
Merge pull request #492 from vshn/feature/cnpg-backup
Feature: CNPG backup
2 parents 85c907d + dd54184 commit a5d575e

File tree

7 files changed

+589
-47
lines changed

7 files changed

+589
-47
lines changed

pkg/comp-functions/functions/common/backup/backup.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func AddK8upBackup(ctx context.Context, svc *runtime.ServiceRuntime, comp common
3737

3838
// Always create/preserve the backup bucket (handles both enabled and disabled states)
3939
l.Info("Creating/preserving backup bucket", "backupEnabled", comp.IsBackupEnabled())
40-
err := createObjectBucket(ctx, comp, svc)
40+
err := CreateObjectBucket(ctx, comp, svc)
4141
if err != nil {
4242
return fmt.Errorf("cannot create/preserve backup bucket: %w", err)
4343
}
@@ -76,7 +76,8 @@ func AddK8upBackup(ctx context.Context, svc *runtime.ServiceRuntime, comp common
7676
return nil
7777
}
7878

79-
func createObjectBucket(ctx context.Context, comp common.InfoGetter, svc *runtime.ServiceRuntime) error {
79+
// Create object bucket for backups
80+
func CreateObjectBucket(ctx context.Context, comp common.InfoGetter, svc *runtime.ServiceRuntime) error {
8081
l := controllerruntime.LoggerFrom(ctx)
8182

8283
if comp.GetName() == "" {
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package vshnpostgrescnpg
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"maps"
7+
"strings"
8+
9+
vshnv1 "github.com/vshn/appcat/v4/apis/vshn/v1"
10+
"github.com/vshn/appcat/v4/pkg/comp-functions/functions/common"
11+
"github.com/vshn/appcat/v4/pkg/comp-functions/functions/common/backup"
12+
"github.com/vshn/appcat/v4/pkg/comp-functions/runtime"
13+
)
14+
15+
// Backup bucket connection details
16+
type backupCredentials struct {
17+
endpoint string
18+
bucket string
19+
region string
20+
accessId string
21+
accessKey string
22+
}
23+
24+
// Bootstrap backup (if enabled)
25+
func SetupBackup(ctx context.Context, svc *runtime.ServiceRuntime, comp *vshnv1.VSHNPostgreSQL, values map[string]any) error {
26+
// CreateObjectBucket has its own IsBackupEnabled to deal with bucket retention
27+
if err := backup.CreateObjectBucket(ctx, comp, svc); err != nil {
28+
return err
29+
}
30+
31+
maintTime := common.SetRandomMaintenanceSchedule(comp)
32+
common.SetRandomBackupSchedule(comp, &maintTime)
33+
34+
if comp.IsBackupEnabled() {
35+
if err := insertBackupValues(svc, comp, values); err != nil {
36+
return err
37+
}
38+
}
39+
return nil
40+
}
41+
42+
// Add backup config to helm values
43+
func insertBackupValues(svc *runtime.ServiceRuntime, comp *vshnv1.VSHNPostgreSQL, values map[string]any) error {
44+
connectionDetails, err := getBackupBucketConnectionDetails(svc, comp)
45+
if err != nil {
46+
return err
47+
}
48+
49+
retention := comp.GetBackupRetention()
50+
retentionDays := retention.KeepDaily
51+
if retentionDays <= 0 {
52+
retentionDays = 6
53+
}
54+
55+
maps.Copy(values, map[string]any{
56+
"backups": map[string]any{
57+
"enabled": true,
58+
"endpointURL": connectionDetails.endpoint,
59+
"retentionPolicy": fmt.Sprintf("%dd", retentionDays),
60+
"scheduledBackups": []map[string]string{{
61+
"name": "default",
62+
"method": "barmanObjectStore",
63+
"schedule": transformSchedule(comp.GetBackupSchedule()),
64+
"backupOwnerReference": "self",
65+
}},
66+
"data": map[string]string{
67+
"encryption": "",
68+
},
69+
"wal": map[string]string{
70+
"encryption": "",
71+
},
72+
"s3": map[string]string{
73+
"bucket": connectionDetails.bucket,
74+
"region": connectionDetails.region,
75+
"accessKey": connectionDetails.accessId,
76+
"secretKey": connectionDetails.accessKey,
77+
// The S3 secret MUST have the keys ACCESS_KEY_ID and ACCESS_SECRET_KEY.
78+
// This is currently hardcoded in the chart, and as the CD secret does not have those keys in verbatim,
79+
// we are forced to pass those values to the chart and letting it create its own secret instead.
80+
},
81+
},
82+
})
83+
84+
return nil
85+
}
86+
87+
func getBackupBucketConnectionDetails(svc *runtime.ServiceRuntime, comp *vshnv1.VSHNPostgreSQL) (backupCredentials, error) {
88+
backupCredentials := backupCredentials{}
89+
cd, err := svc.GetObservedComposedResourceConnectionDetails(comp.GetName() + "-backup")
90+
if err != nil && err == runtime.ErrNotFound {
91+
return backupCredentials, fmt.Errorf("backup bucket connection details not found")
92+
} else if err != nil {
93+
return backupCredentials, err
94+
}
95+
96+
endpoint, _ := strings.CutSuffix(string(cd["ENDPOINT_URL"]), "/")
97+
backupCredentials.endpoint = endpoint
98+
backupCredentials.bucket = string(cd["BUCKET_NAME"])
99+
backupCredentials.region = string(cd["AWS_REGION"])
100+
backupCredentials.accessId = string(cd["AWS_ACCESS_KEY_ID"])
101+
backupCredentials.accessKey = string(cd["AWS_SECRET_ACCESS_KEY"])
102+
return backupCredentials, nil
103+
}
104+
105+
// Transform backup schedule according to robfig/cron (used by CNPG)
106+
// https://pkg.go.dev/github.com/robfig/cron#hdr-CRON_Expression_Format
107+
func transformSchedule(thisSchedule string) string {
108+
return fmt.Sprintf("0 %s", thisSchedule)
109+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package vshnpostgrescnpg
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
appcatv1 "github.com/vshn/appcat/v4/apis/v1"
9+
"github.com/vshn/appcat/v4/pkg/comp-functions/runtime"
10+
)
11+
12+
func Test_BackupBootstrapDisabled(t *testing.T) {
13+
svc, comp := getPostgreSqlComp(t, "vshn-postgres/deploy/05_backup_disabled_cnpg.yaml")
14+
ctx := context.TODO()
15+
16+
// If backup has been disabled and the instance first created, expect neither bucket nor backup values
17+
values, err := createCnpgHelmValues(ctx, svc, comp)
18+
assert.NoError(t, err)
19+
20+
assert.NoError(t, SetupBackup(ctx, svc, comp, values))
21+
assert.Nil(t, values["backups"])
22+
23+
bucketName := comp.GetName() + "-backup"
24+
err = svc.GetDesiredComposedResourceByName(&appcatv1.XObjectBucket{}, bucketName)
25+
assert.ErrorIs(t, err, runtime.ErrNotFound)
26+
}
27+
28+
func TestBackupBooststrapEnabled(t *testing.T) {
29+
svc, comp := getPostgreSqlComp(t, "vshn-postgres/deploy/05_backup_cnpg.yaml")
30+
ctx := context.TODO()
31+
32+
// If backup has been enabled, expect values and backup bucket
33+
values, err := createCnpgHelmValues(ctx, svc, comp)
34+
assert.NoError(t, err)
35+
36+
assert.NoError(t, SetupBackup(ctx, svc, comp, values))
37+
assert.NotNil(t, values["backups"])
38+
39+
backupValues := values["backups"].(map[string]any)
40+
41+
// Enabled, retention
42+
assert.True(t, backupValues["enabled"].(bool))
43+
assert.Equal(t, backupValues["retentionPolicy"], "6d")
44+
45+
// Schedule
46+
assert.Equal(t,
47+
backupValues["scheduledBackups"].([]map[string]string)[0]["schedule"],
48+
transformSchedule(comp.GetBackupSchedule()),
49+
)
50+
51+
// Bucket configuration
52+
cd, err := getBackupBucketConnectionDetails(svc, comp)
53+
assert.NoError(t, err)
54+
assert.Equal(t, cd.endpoint, "https://s3.minio.local") // No trailing /
55+
assert.Equal(t, cd.bucket, "backupBucket")
56+
assert.Equal(t, cd.region, "rma")
57+
assert.Equal(t, cd.accessId, "secretAccessId")
58+
assert.Equal(t, cd.accessKey, "secretAccessKey")
59+
60+
assert.Equal(t, cd.endpoint, backupValues["endpointURL"])
61+
assert.Equal(t, cd.bucket, backupValues["s3"].(map[string]string)["bucket"])
62+
assert.Equal(t, cd.region, backupValues["s3"].(map[string]string)["region"])
63+
assert.Equal(t, cd.accessId, backupValues["s3"].(map[string]string)["accessKey"])
64+
assert.Equal(t, cd.accessKey, backupValues["s3"].(map[string]string)["secretKey"])
65+
66+
bucketName := comp.GetName() + "-backup"
67+
err = svc.GetDesiredComposedResourceByName(&appcatv1.XObjectBucket{}, bucketName)
68+
assert.NoError(t, err)
69+
}

pkg/comp-functions/functions/vshnpostgrescnpg/deploy.go

Lines changed: 4 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ import (
1111

1212
cmv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
1313
certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
14-
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
1514
xfnproto "github.com/crossplane/function-sdk-go/proto/v1"
16-
appcatv1 "github.com/vshn/appcat/v4/apis/v1"
1715
vshnv1 "github.com/vshn/appcat/v4/apis/vshn/v1"
1816
k8sruntime "k8s.io/apimachinery/pkg/runtime"
1917

@@ -141,49 +139,6 @@ func createCerts(comp *vshnv1.VSHNPostgreSQL, svc *runtime.ServiceRuntime) error
141139
return nil
142140
}
143141

144-
func createObjectBucket(comp *vshnv1.VSHNPostgreSQL, svc *runtime.ServiceRuntime) error {
145-
xObjectBucket := &appcatv1.XObjectBucket{
146-
ObjectMeta: metav1.ObjectMeta{
147-
Name: comp.GetName(),
148-
Labels: map[string]string{
149-
runtime.ProviderConfigIgnoreLabel: "true",
150-
},
151-
},
152-
Spec: appcatv1.XObjectBucketSpec{
153-
Parameters: appcatv1.ObjectBucketParameters{
154-
BucketName: fmt.Sprintf("%s-%s-%s", comp.GetName(), svc.Config.Data["bucketRegion"], "backup"),
155-
},
156-
ResourceSpec: xpv1.ResourceSpec{
157-
WriteConnectionSecretToReference: &xpv1.SecretReference{
158-
Name: "pgbucket-" + comp.GetName(),
159-
Namespace: svc.GetCrossplaneNamespace(),
160-
},
161-
},
162-
},
163-
}
164-
165-
xObjectBucket.Spec.Parameters.BucketName = getBucketName(svc, xObjectBucket)
166-
167-
err := svc.SetDesiredComposedResourceWithName(xObjectBucket, "pg-bucket")
168-
if err != nil {
169-
err = fmt.Errorf("cannot create xObjectBucket: %w", err)
170-
return err
171-
}
172-
173-
return nil
174-
}
175-
176-
func getBucketName(svc *runtime.ServiceRuntime, currentBucket *appcatv1.XObjectBucket) string {
177-
bucket := &appcatv1.XObjectBucket{}
178-
179-
err := svc.GetObservedComposedResource(bucket, "pg-bucket")
180-
if err != nil {
181-
return currentBucket.Spec.Parameters.BucketName
182-
}
183-
184-
return bucket.Spec.Parameters.BucketName
185-
}
186-
187142
// Deploy PostgresQL using the CNPG cluster helm chart
188143
func deployPostgresSQLUsingCNPG(ctx context.Context, comp *vshnv1.VSHNPostgreSQL, svc *runtime.ServiceRuntime) *xfnproto.Result {
189144
// Deploy
@@ -192,6 +147,10 @@ func deployPostgresSQLUsingCNPG(ctx context.Context, comp *vshnv1.VSHNPostgreSQL
192147
return runtime.NewFatalResult(fmt.Errorf("cannot create helm values: %w", err))
193148
}
194149

150+
if err := SetupBackup(ctx, svc, comp, values); err != nil {
151+
return runtime.NewWarningResult(fmt.Sprintf("cannot set up backup: %v", err))
152+
}
153+
195154
svc.Log.Info("Creating Helm release for CNPG PostgreSQL")
196155
release, err := common.NewRelease(ctx, svc, comp, values, comp.GetName()+"-cnpg")
197156
if err != nil {

pkg/comp-functions/functions/vshnpostgrescnpg/deploy_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
vshnv1 "github.com/vshn/appcat/v4/apis/vshn/v1"
99
"github.com/vshn/appcat/v4/pkg/comp-functions/functions/commontest"
1010
"github.com/vshn/appcat/v4/pkg/comp-functions/runtime"
11+
"k8s.io/utils/ptr"
1112
)
1213

1314
const (
@@ -19,6 +20,7 @@ func Test_deploy(t *testing.T) {
1920
svc, comp := getSvcCompCnpg(t)
2021
ctx := context.TODO()
2122

23+
comp.Spec.Parameters.Backup.Enabled = ptr.To(false)
2224
assert.Nil(t, deployPostgresSQLUsingCNPG(ctx, comp, svc))
2325
}
2426

0 commit comments

Comments
 (0)