Fix leader-election RBAC, Go version drift, PVC churn, cron names, apply order
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful

Addresses PR review + acceptance findings:

- Add leader-election RBAC: namespaced Role (coordination leases +
  events) and RoleBinding to the operator ServiceAccount, so controllers
  actually start under --leader-elect instead of looping on "leases
  forbidden". Verified in a kind cluster: lease acquired, both
  controllers start workers.
- Align Go versions: bump Dockerfile.operator to golang:1.26-alpine and
  CI images to golang:1.26 to match go.mod (go 1.26.5); set
  GOTOOLCHAIN=local so the image build stays hermetic (no toolchain
  download).
- PVC controller: add an annotation predicate so only PVCs carrying (or
  transitioning off of) backups.unkin.net/schedule enqueue, eliminating
  reconcile churn from unannotated PVCs while keeping the teardown path.
- Cron validator: accept alphabetic month/day-of-week names (MON, JAN,
  MON-FRI) that k8up and CNPG's robfig/cron accept, still rejecting
  unknown names; add unit tests for both mapping paths.
- Deploy ordering: extract the Namespace into its own manifest and add a
  config/kustomization.yaml so `kubectl apply -k config` creates the
  namespace first; document it in the README.
- Note the credential-less push precedent (jellyfin-ha) in docker.yaml.
This commit is contained in:
2026-08-14 00:33:49 +10:00
parent 9da206dc7c
commit a3339a30b5
14 changed files with 195 additions and 15 deletions
+39 -4
View File
@@ -76,6 +76,14 @@ func ScheduleForCNPG(in string) (string, error) {
}
}
// cronNames are the alphabetic month and day-of-week names robfig/cron (used by
// k8up and CloudNativePG) accepts, case-insensitively, e.g. "MON" or "JAN".
var cronNames = map[string]bool{
"JAN": true, "FEB": true, "MAR": true, "APR": true, "MAY": true, "JUN": true,
"JUL": true, "AUG": true, "SEP": true, "OCT": true, "NOV": true, "DEC": true,
"SUN": true, "MON": true, "TUE": true, "WED": true, "THU": true, "FRI": true, "SAT": true,
}
func validateCronFields(s string, want int) error {
fields := strings.Fields(s)
if len(fields) != want {
@@ -85,15 +93,42 @@ func validateCronFields(s string, want int) error {
if f == "" {
return fmt.Errorf("cron %q has an empty field", s)
}
for _, r := range f {
if !isCronRune(r) {
return fmt.Errorf("cron field %q contains invalid character %q", f, string(r))
}
if err := validateCronField(f); err != nil {
return err
}
}
return nil
}
// validateCronField accepts cron symbols plus known alphabetic month/day names
// (e.g. MON-FRI, JAN); an unknown alphabetic run like "foo" is rejected.
func validateCronField(f string) error {
runes := []rune(f)
for i := 0; i < len(runes); {
if isLetter(runes[i]) {
j := i
for j < len(runes) && isLetter(runes[j]) {
j++
}
name := strings.ToUpper(string(runes[i:j]))
if !cronNames[name] {
return fmt.Errorf("cron field %q contains unknown name %q", f, string(runes[i:j]))
}
i = j
continue
}
if !isCronRune(runes[i]) {
return fmt.Errorf("cron field %q contains invalid character %q", f, string(runes[i]))
}
i++
}
return nil
}
func isCronRune(r rune) bool {
return (r >= '0' && r <= '9') || r == '*' || r == '/' || r == ',' || r == '-' || r == '?'
}
func isLetter(r rune) bool {
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
}