Files
unkin-agent 9da206dc7c Implement autobackup-operator controllers, tests, CI and packaging
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.
2026-08-14 00:08:37 +10:00

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
}