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

187 lines
4.2 KiB
Go

// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package mangling
import (
"bytes"
"strings"
"unicode"
"unicode/utf8"
)
type (
lexemKind uint8
nameLexem struct {
original string
matchedInitialism string
kind lexemKind
}
)
const (
lexemKindCasualName lexemKind = iota
lexemKindInitialismName
)
func newInitialismNameLexem(original, matchedInitialism string) nameLexem {
return nameLexem{
kind: lexemKindInitialismName,
original: original,
matchedInitialism: matchedInitialism,
}
}
func newCasualNameLexem(original string) nameLexem {
return nameLexem{
kind: lexemKindCasualName,
original: trim(original), // TODO: save on calls to trim
}
}
// WriteTitleized writes the titleized lexeme to a bytes.Buffer.
//
// If the first letter cannot be capitalized, it doesn't write anything and return false,
// so the caller may attempt some workaround strategy.
func (l nameLexem) WriteTitleized(w *bytes.Buffer, alwaysUpper bool) bool {
if l.kind == lexemKindInitialismName {
w.WriteString(l.matchedInitialism)
return true
}
if len(l.original) == 0 {
return true
}
if len(l.original) == 1 {
// identifier is too short: casing will depend on the context
firstByte := l.original[0]
switch {
case 'A' <= firstByte && firstByte <= 'Z':
// safe
w.WriteByte(firstByte)
return true
case alwaysUpper && 'a' <= firstByte && firstByte <= 'z':
w.WriteByte(firstByte - 'a' + 'A')
return true
default:
// not a letter: skip and let the caller decide
return false
}
}
if firstByte := l.original[0]; firstByte < utf8.RuneSelf {
// ASCII
switch {
case 'A' <= firstByte && firstByte <= 'Z':
// already an upper case letter
w.WriteString(l.original)
return true
case 'a' <= firstByte && firstByte <= 'z':
w.WriteByte(firstByte - 'a' + 'A')
w.WriteString(l.original[1:])
return true
default:
// not a good candidate: doesn't start with a letter
return false
}
}
// unicode
firstRune, idx := utf8.DecodeRuneInString(l.original)
if !unicode.IsLetter(firstRune) || !unicode.IsUpper(unicode.ToUpper(firstRune)) {
// not a good candidate: doesn't start with a letter
// or a rune for which case doesn't make sense (e.g. East-Asian runes etc)
return false
}
rest := l.original[idx:]
w.WriteRune(unicode.ToUpper(firstRune))
w.WriteString(strings.ToLower(rest))
return true
}
// WriteLower is like write titleized but it writes a lower-case version of the lexeme.
//
// Similarly, there is no writing if the casing of the first rune doesn't make sense.
func (l nameLexem) WriteLower(w *bytes.Buffer, alwaysLower bool) bool {
if l.kind == lexemKindInitialismName {
w.WriteString(lower(l.matchedInitialism))
return true
}
if len(l.original) == 0 {
return true
}
if len(l.original) == 1 {
// identifier is too short: casing will depend on the context
firstByte := l.original[0]
switch {
case 'a' <= firstByte && firstByte <= 'z':
// safe
w.WriteByte(firstByte)
return true
case alwaysLower && 'A' <= firstByte && firstByte <= 'Z':
w.WriteByte(firstByte - 'A' + 'a')
return true
default:
// not a letter: skip and let the caller decide
return false
}
}
if firstByte := l.original[0]; firstByte < utf8.RuneSelf {
// ASCII
switch {
case 'a' <= firstByte && firstByte <= 'z':
// already a lower case letter
w.WriteString(l.original)
return true
case 'A' <= firstByte && firstByte <= 'Z':
w.WriteByte(firstByte - 'A' + 'a')
w.WriteString(l.original[1:])
return true
default:
// not a good candidate: doesn't start with a letter
return false
}
}
// unicode
firstRune, idx := utf8.DecodeRuneInString(l.original)
if !unicode.IsLetter(firstRune) || !unicode.IsLower(unicode.ToLower(firstRune)) {
// not a good candidate: doesn't start with a letter
// or a rune for which case doesn't make sense (e.g. East-Asian runes etc)
return false
}
rest := l.original[idx:]
w.WriteRune(unicode.ToLower(firstRune))
w.WriteString(rest)
return true
}
func (l nameLexem) GetOriginal() string {
return l.original
}
func (l nameLexem) IsInitialism() bool {
return l.kind == lexemKindInitialismName
}