9da206dc7c
PVCs and CloudNativePG Clusters need S3 buckets and backup schedules
provisioned consistently. This operator watches the
backups.unkin.net/{schedule,destination} annotations on those objects and
provisions everything needed to back them up, with no new CRDs.
- Add a PVC controller that provisions cephrgw ObjectStoreUser/Bucket/BucketAccess,
auto-generates a restic repo-password Secret and creates a k8up Schedule scoped
to the PVC via spec.backup.volumes[].persistentVolumeClaim.claimName.
- Add a CNPG Cluster controller that provisions the same bucket stack, idempotently
patches spec.backup.barmanObjectStore (leaving a user-set destinationPath alone
with a Warning event) and creates a ScheduledBackup.
- Resolve destinations through a ConfigMap lookup table; requeue until the
BucketAccess is Ready before creating schedule resources; own-reference created
resources and retain bucket data by default.
- Add schedule-mapping helpers (k8up 5-field/shortcut pass-through, CNPG 6-field
seconds-first) and deterministic, length-bounded name derivation.
- Add unit tests (schedule mapping, name derivation, destination resolution) and
envtest controller tests for both paths, wiring the external CRDs into envtest.
- Add kubebuilder-generated RBAC, a Dockerfile (distroless/nonroot), Woodpecker
lint/test/build pipelines and a tag-triggered image push to the artifactapi
docker-internal registry, plus a version-bump Makefile and deploy manifests.
71 lines
2.6 KiB
Go
71 lines
2.6 KiB
Go
package controller
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
|
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
|
)
|
|
|
|
func destClient(t *testing.T, objs ...runtime.Object) *fake.ClientBuilder {
|
|
t.Helper()
|
|
return fake.NewClientBuilder().WithScheme(clientgoscheme.Scheme).WithRuntimeObjects(objs...)
|
|
}
|
|
|
|
func TestResolveDestinationHit(t *testing.T) {
|
|
cm := &corev1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "autobackup-destinations", Namespace: "autobackup-operator"},
|
|
Data: map[string]string{
|
|
"cephs3_ec4_1": "placementTarget: cephs3_ec4_1\nzonegroup: au\nendpoint: https://s3.ceph.unkin.net\nendpointCASecret: vault-ca-cert\nendpointCAKey: ca.crt\n",
|
|
},
|
|
}
|
|
c := destClient(t, cm).Build()
|
|
d, err := resolveDestination(context.Background(), c, "autobackup-operator", "autobackup-destinations", "cephs3_ec4_1")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if d.PlacementTarget != "cephs3_ec4_1" || d.Zonegroup != "au" || d.Endpoint != "https://s3.ceph.unkin.net" {
|
|
t.Errorf("unexpected destination: %+v", d)
|
|
}
|
|
if d.EndpointCASecret != "vault-ca-cert" || d.EndpointCAKey != "ca.crt" {
|
|
t.Errorf("unexpected CA fields: %+v", d)
|
|
}
|
|
}
|
|
|
|
func TestResolveDestinationMissEntry(t *testing.T) {
|
|
cm := &corev1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "autobackup-destinations", Namespace: "autobackup-operator"},
|
|
Data: map[string]string{"other": "endpoint: https://x\n"},
|
|
}
|
|
c := destClient(t, cm).Build()
|
|
_, err := resolveDestination(context.Background(), c, "autobackup-operator", "autobackup-destinations", "cephs3_ec4_1")
|
|
if !errors.Is(err, errDestinationNotFound) {
|
|
t.Errorf("expected errDestinationNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestResolveDestinationMissConfigMap(t *testing.T) {
|
|
c := destClient(t).Build()
|
|
_, err := resolveDestination(context.Background(), c, "autobackup-operator", "autobackup-destinations", "cephs3_ec4_1")
|
|
if !errors.Is(err, errDestinationNotFound) {
|
|
t.Errorf("expected errDestinationNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestResolveDestinationMissingEndpoint(t *testing.T) {
|
|
cm := &corev1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "autobackup-destinations", Namespace: "autobackup-operator"},
|
|
Data: map[string]string{"noendpoint": "placementTarget: x\n"},
|
|
}
|
|
c := destClient(t, cm).Build()
|
|
_, err := resolveDestination(context.Background(), c, "autobackup-operator", "autobackup-destinations", "noendpoint")
|
|
if err == nil {
|
|
t.Errorf("expected error for missing endpoint")
|
|
}
|
|
}
|