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.
65 lines
2.1 KiB
Go
65 lines
2.1 KiB
Go
package controller
|
|
|
|
import (
|
|
"crypto/sha1"
|
|
"encoding/hex"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
// maxObjectName is the DNS-1123 subdomain limit for Kubernetes object names.
|
|
maxObjectName = 253
|
|
// maxBucketName is the DNS-1123 label limit RGW enforces on S3 bucket names.
|
|
maxBucketName = 63
|
|
namePrefix = "autobk"
|
|
)
|
|
|
|
var invalidNameChars = regexp.MustCompile(`[^a-z0-9-]+`)
|
|
|
|
// bucketName derives the namespace-scoped, globally-unique S3 bucket name (and
|
|
// the Bucket CR name) for an annotated object, clamped to the 63-char RGW limit.
|
|
func bucketName(namespace, obj string) string {
|
|
return boundedName(maxBucketName, namePrefix, namespace, obj)
|
|
}
|
|
|
|
// userName, accessName, credSecretName, resticSecretName and scheduleName derive
|
|
// the per-object child resource names. These are namespaced objects, so the
|
|
// namespace is not part of the name (only uniqueness within the namespace is
|
|
// required).
|
|
func userName(obj string) string { return boundedName(maxObjectName, namePrefix, obj, "owner") }
|
|
func accessName(obj string) string { return boundedName(maxObjectName, namePrefix, obj, "rw") }
|
|
func credSecretName(obj string) string { return boundedName(maxObjectName, obj, "autobackup", "rgw") }
|
|
func resticSecretName(obj string) string {
|
|
return boundedName(maxObjectName, obj, "autobackup", "restic")
|
|
}
|
|
func scheduleName(obj string) string { return boundedName(maxObjectName, obj, "autobackup") }
|
|
|
|
// boundedName joins parts with "-", sanitises to a DNS-safe token and, when the
|
|
// result would exceed max, truncates it and appends a short deterministic hash
|
|
// of the full joined value so long inputs stay unique and within limits.
|
|
func boundedName(max int, parts ...string) string {
|
|
joined := strings.Join(parts, "-")
|
|
s := sanitizeName(joined)
|
|
if len(s) <= max {
|
|
return s
|
|
}
|
|
sum := sha1.Sum([]byte(joined))
|
|
h := hex.EncodeToString(sum[:])[:8]
|
|
keep := max - len(h) - 1
|
|
if keep < 1 {
|
|
return h[:max]
|
|
}
|
|
return strings.TrimRight(s[:keep], "-") + "-" + h
|
|
}
|
|
|
|
func sanitizeName(s string) string {
|
|
s = strings.ToLower(s)
|
|
s = invalidNameChars.ReplaceAllString(s, "-")
|
|
s = strings.Trim(s, "-")
|
|
if s == "" {
|
|
return namePrefix
|
|
}
|
|
return s
|
|
}
|