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

209 lines
5.8 KiB
Go

/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package log
import (
"sync"
"github.com/go-logr/logr"
)
// loggerPromise knows how to populate a concrete logr.Logger
// with options, given an actual base logger later on down the line.
type loggerPromise struct {
logger *delegatingLogSink
childPromises []*loggerPromise
promisesLock sync.Mutex
name *string
tags []any
}
func (p *loggerPromise) WithName(l *delegatingLogSink, name string) *loggerPromise {
res := &loggerPromise{
logger: l,
name: &name,
promisesLock: sync.Mutex{},
}
p.promisesLock.Lock()
defer p.promisesLock.Unlock()
p.childPromises = append(p.childPromises, res)
return res
}
// WithValues provides a new Logger with the tags appended.
func (p *loggerPromise) WithValues(l *delegatingLogSink, tags ...any) *loggerPromise {
res := &loggerPromise{
logger: l,
tags: tags,
promisesLock: sync.Mutex{},
}
p.promisesLock.Lock()
defer p.promisesLock.Unlock()
p.childPromises = append(p.childPromises, res)
return res
}
// Fulfill instantiates the Logger with the provided logger.
func (p *loggerPromise) Fulfill(parentLogSink logr.LogSink) {
sink := parentLogSink
if p.name != nil {
sink = sink.WithName(*p.name)
}
if p.tags != nil {
sink = sink.WithValues(p.tags...)
}
p.logger.lock.Lock()
p.logger.logger = sink
if withCallDepth, ok := sink.(logr.CallDepthLogSink); ok {
p.logger.logger = withCallDepth.WithCallDepth(1)
}
p.logger.promise = nil
p.logger.lock.Unlock()
for _, childPromise := range p.childPromises {
childPromise.Fulfill(sink)
}
}
// delegatingLogSink is a logsink that delegates to another logr.LogSink.
// If the underlying promise is not nil, it registers calls to sub-loggers with
// the logging factory to be populated later, and returns a new delegating
// logger. It expects to have *some* logr.Logger set at all times (generally
// a no-op logger before the promises are fulfilled).
type delegatingLogSink struct {
lock sync.RWMutex
logger logr.LogSink
promise *loggerPromise
info logr.RuntimeInfo
}
// Init implements logr.LogSink.
func (l *delegatingLogSink) Init(info logr.RuntimeInfo) {
eventuallyFulfillRoot()
l.lock.Lock()
defer l.lock.Unlock()
l.info = info
}
// Enabled tests whether this Logger is enabled. For example, commandline
// flags might be used to set the logging verbosity and disable some info
// logs.
func (l *delegatingLogSink) Enabled(level int) bool {
eventuallyFulfillRoot()
l.lock.RLock()
defer l.lock.RUnlock()
return l.logger.Enabled(level)
}
// Info logs a non-error message with the given key/value pairs as context.
//
// The msg argument should be used to add some constant description to
// the log line. The key/value pairs can then be used to add additional
// variable information. The key/value pairs should alternate string
// keys and arbitrary values.
func (l *delegatingLogSink) Info(level int, msg string, keysAndValues ...any) {
eventuallyFulfillRoot()
l.lock.RLock()
defer l.lock.RUnlock()
l.logger.Info(level, msg, keysAndValues...)
}
// Error logs an error, with the given message and key/value pairs as context.
// It functions similarly to calling Info with the "error" named value, but may
// have unique behavior, and should be preferred for logging errors (see the
// package documentations for more information).
//
// The msg field should be used to add context to any underlying error,
// while the err field should be used to attach the actual error that
// triggered this log line, if present.
func (l *delegatingLogSink) Error(err error, msg string, keysAndValues ...any) {
eventuallyFulfillRoot()
l.lock.RLock()
defer l.lock.RUnlock()
l.logger.Error(err, msg, keysAndValues...)
}
// WithName provides a new Logger with the name appended.
func (l *delegatingLogSink) WithName(name string) logr.LogSink {
eventuallyFulfillRoot()
l.lock.RLock()
defer l.lock.RUnlock()
if l.promise == nil {
sink := l.logger.WithName(name)
if withCallDepth, ok := sink.(logr.CallDepthLogSink); ok {
sink = withCallDepth.WithCallDepth(-1)
}
return sink
}
res := &delegatingLogSink{logger: l.logger}
promise := l.promise.WithName(res, name)
res.promise = promise
return res
}
// WithValues provides a new Logger with the tags appended.
func (l *delegatingLogSink) WithValues(tags ...any) logr.LogSink {
eventuallyFulfillRoot()
l.lock.RLock()
defer l.lock.RUnlock()
if l.promise == nil {
sink := l.logger.WithValues(tags...)
if withCallDepth, ok := sink.(logr.CallDepthLogSink); ok {
sink = withCallDepth.WithCallDepth(-1)
}
return sink
}
res := &delegatingLogSink{logger: l.logger}
promise := l.promise.WithValues(res, tags...)
res.promise = promise
return res
}
// Fulfill switches the logger over to use the actual logger
// provided, instead of the temporary initial one, if this method
// has not been previously called.
func (l *delegatingLogSink) Fulfill(actual logr.LogSink) {
if actual == nil {
actual = NullLogSink{}
}
if l.promise != nil {
l.promise.Fulfill(actual)
}
}
// newDelegatingLogSink constructs a new DelegatingLogSink which uses
// the given logger before its promise is fulfilled.
func newDelegatingLogSink(initial logr.LogSink) *delegatingLogSink {
l := &delegatingLogSink{
logger: initial,
promise: &loggerPromise{promisesLock: sync.Mutex{}},
}
l.promise.logger = l
return l
}