Files
cozystack/internal/backupcontroller/credentials_projector_runnable.go
Aleksei Sviridkin 7a0aab2bdc refactor: apply the Go 1.27 modernizers
Run go fix over every first-party module: interface{} becomes any,
hand-written loops become slices, maps, min/max and range-over-int,
errors.As becomes errors.AsType, pointer helpers become new(expr) and
are inlined at their call sites, and so on. Every rewrite keeps
behaviour.

Three rewrites stay out. embedlit flattens embedded-field literals
such as ObjectMeta, syntax only Go 1.27 accepts, which would break
backports to release branches still on Go 1.26. omitzero drops
omitempty from struct fields, and openapi-gen reads omitempty as
optional, so metadata, spec and status would turn required in the
published schemas. And errorsastype rewrote an errors.As whose
target, apierrors.APIStatus, does not implement error, which
errors.AsType rejects at compile time.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-24 17:23:18 +03:00

146 lines
5.4 KiB
Go

package backupcontroller
import (
"context"
"errors"
"strings"
"time"
"github.com/go-logr/logr"
"github.com/prometheus/client_golang/prometheus"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/metrics"
)
// credentialsProjectionFailures counts projection errors per target
// namespace and reason. Exposes terminal misconfigurations (malformed
// source Secret, unowned target) as a Kubernetes-native signal rather
// than a log line so operators see them in the same Grafana board the
// rest of the controller's metrics land in. Reset to zero on success
// would mask repeated transient failures, so this is a monotonic counter
// — alerting is on rate(...) or absent successes.
var credentialsProjectionFailures = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "cozystack_backup_credentials_projection_failures_total",
Help: "Number of failed BackupCredentials projections, by target namespace and reason.",
},
[]string{"namespace", "reason"},
)
// credentialsProjectionSuccesses counts successful projection ticks per
// target namespace. Combined with the failures counter, operators can
// alert on absent successes (BSL going stale) without log scraping.
var credentialsProjectionSuccesses = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "cozystack_backup_credentials_projection_successes_total",
Help: "Number of successful BackupCredentials projections, by target namespace.",
},
[]string{"namespace"},
)
func init() {
metrics.Registry.MustRegister(credentialsProjectionFailures, credentialsProjectionSuccesses)
}
// SystemCredentialsProjector is a controller-runtime Runnable that
// projects the platform-managed backup credentials Secret into a list of
// system namespaces (e.g. cozy-velero, where Velero's BSL references a
// Secret-backed credential file).
//
// Tenant namespaces are handled lazily by the BackupJob/RestoreJob
// reconcilers — they project on demand right before each Job runs. System
// namespaces, by contrast, are referenced by long-lived static
// configuration (BackupStorageLocation, operator deployments) that must
// have credentials available at all times, so the runnable refreshes them
// on a periodic tick instead.
type SystemCredentialsProjector struct {
Client client.Client
Config BackupCredentialsConfig
Namespaces []string
Period time.Duration
}
var (
_ manager.Runnable = (*SystemCredentialsProjector)(nil)
_ manager.LeaderElectionRunnable = (*SystemCredentialsProjector)(nil)
)
// NeedLeaderElection makes the projector leader-elected — only one
// controller-manager replica ticks against the apiserver per cycle.
// Without this, every replica (the deployment runs 2) would race to
// project the same Secret every minute, doubling apiserver load and
// inflating the success counter with duplicate increments.
func (p *SystemCredentialsProjector) NeedLeaderElection() bool { return true }
// NewSystemCredentialsProjector parses a comma-separated namespace list
// and returns a configured runnable. An empty namespaces list disables
// the runnable — Start returns immediately. Defaults to a 1-minute tick
// if Period is zero.
func NewSystemCredentialsProjector(c client.Client, cfg BackupCredentialsConfig, namespacesCSV string, period time.Duration) *SystemCredentialsProjector {
var ns []string
for s := range strings.SplitSeq(namespacesCSV, ",") {
s = strings.TrimSpace(s)
if s != "" {
ns = append(ns, s)
}
}
if period == 0 {
period = time.Minute
}
return &SystemCredentialsProjector{
Client: c,
Config: cfg,
Namespaces: ns,
Period: period,
}
}
func (p *SystemCredentialsProjector) Start(ctx context.Context) error {
logger := log.FromContext(ctx).WithName("system-credentials-projector")
if len(p.Namespaces) == 0 || !p.Config.IsEnabled() {
logger.V(1).Info("system credentials projector disabled", "namespaces", p.Namespaces, "configured", p.Config.IsEnabled())
return nil
}
tick := time.NewTicker(p.Period)
defer tick.Stop()
p.projectAll(ctx, logger)
for {
select {
case <-ctx.Done():
return nil
case <-tick.C:
p.projectAll(ctx, logger)
}
}
}
func (p *SystemCredentialsProjector) projectAll(ctx context.Context, logger logr.Logger) {
for _, ns := range p.Namespaces {
if err := ProjectBackupCredentials(ctx, p.Client, p.Config, ns); err != nil {
reason := classifyReason(err)
credentialsProjectionFailures.WithLabelValues(ns, reason).Inc()
// Always log at Info, not Error — the Prometheus counter is
// the actionable signal (rate(failures_total{reason="..."}))
// so a malformed source Secret does not spam Error logs every
// minute. Operators alert on the counter; the log line is
// kept for diagnostic context only.
logger.Info("system credentials projection failed", "namespace", ns, "reason", reason, "error", err.Error())
continue
}
credentialsProjectionSuccesses.WithLabelValues(ns).Inc()
logger.V(1).Info("system credentials projection succeeded", "namespace", ns)
}
}
// classifyReason returns the ProjectionError reason if the underlying
// error is one of ours, otherwise "Unknown". Used as a metric label, so
// the cardinality is bounded by the small Reason* set.
func classifyReason(err error) string {
if perr, ok := errors.AsType[*ProjectionError](err); ok {
return perr.Reason
}
return "Unknown"
}