mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-28 07:37:49 +09:00
## What this PR does This replaces the open Renovate PRs with one rollup. Go modules move to their newest releases that keep the Kubernetes libraries and controller-runtime where they are, the toolchain and builder images move to Go 1.27.1, and the first-party code gets the Go 1.27 modernizers. Tests for the code the bumps touch come first, in their own commits, and pass on both the old and the new dependencies. Before the bumps: - The e2e sandbox image builds again: dl.min.io answers 410 for mc since MinIO archived it, so mc now comes from its last GitHub release with a pinned checksum. - Renovate stops offering tags of the apimachinery fork. They mirror upstream, so #2999 is a downgrade that drops our patch. - `make unit-tests` now runs the tests of the nested Go modules (kubevirt-csi-driver, token-proxy, kubeovn-webhook, api/apps/v1alpha1). They never ran in CI before. - New tests: JWT verification in the dashboard token proxy, the kube-ovn plunger metrics, cert-manager object idempotency in the tenant gateway, values-file layering in the operator, and the kubevirt CSI driver wrapper. - One real fix. The lineage webhook decoded admitted objects with encoding/json, so an int64 above 2^53 came back rounded. jsonpatch v2.5 compares numbers exactly and would emit a replace that rewrites such a field in the pod. The webhook now decodes with apimachinery's json. Held back on purpose, with the reasons in the commit messages: - Kubernetes libraries stay on v0.35. #3001 needs the apimachinery fork rebased onto release-1.37 first. - cel-go v0.29.2 and etcd v3.6.15, the lines k8s.io/apiserver v0.35 is built and tested with. - grpc v1.83.2. v1.84.0 is affected by GO-2026-6443 and no v1.84 patch release carries the fix yet. - kubevirt.io/api and the CDI API stay on what upstream csi-driver builds against. From CDI v1.60 it no longer compiles. - api/apps/v1alpha1 keeps go 1.26.4, so consumers of the published types are not forced onto Go 1.27. - go fix runs without embedlit (Go 1.27-only syntax, breaks backports to release branches on Go 1.26) and omitzero (would make metadata, spec and status required in the published OpenAPI schemas), and one errorsastype rewrite that did not compile is left out. govulncheck reports nothing reachable in any of the five modules, standard library included. Supersedes #4311, #4310, #4309, #3452, #3993, #3927, #3902, #3805, #3451, #3415, #3414, #3413, #3302, #4050 and #3363. #3001 stays open. Not checked locally: the images that build upstream projects (kube-ovn, kamaji, cilium, linstor-csi and others) with the Go 1.27 builder. The image build jobs of this PR cover them. Closes #4487. ### Screenshots No UI changes. ### Downstream repositories - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: - [ ] [cozystack/community](https://github.com/cozystack/community) - follow-up: ### Release note ```release-note chore(deps): move the toolchain and builder images to Go 1.27.1 and raise Go dependencies to their latest compatible releases, closing the published advisories; the lineage webhook no longer rewrites int64 fields above 2^53 in admitted objects. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a restore resource policy option for Velero strategies. * Added a test target to run Go tests across tracked modules. * **Bug Fixes** * Updated the end-to-end test environment to download and verify the MinIO client from its current release location. * Added coverage for token validation, webhook patching, controller behavior, and reconciliation. * **Maintenance** * Updated the Go toolchain, dependencies, and container image versions. * Refreshed pinned workflow actions and base image digests. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
879 lines
36 KiB
Go
879 lines
36 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
package backupcontroller
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
apimeta "k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/utils/ptr"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
strategyv1alpha1 "github.com/cozystack/cozystack/api/backups/strategy/v1alpha1"
|
|
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
|
|
"github.com/cozystack/cozystack/internal/backupcontroller/mariadbapp"
|
|
"github.com/cozystack/cozystack/internal/backupcontroller/mariadbtypes"
|
|
"github.com/cozystack/cozystack/internal/template"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Constants
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const (
|
|
mariadbFieldManager = "cozystack-mariadb-backup-driver"
|
|
|
|
// mariadb-operator's MariaDB ApplicationDefinition (release.prefix). The
|
|
// cozystack mariadb-rd renders the HelmRelease with releaseName =
|
|
// "mariadb-" + appName (see packages/system/mariadb-rd/cozyrds/mariadb.yaml,
|
|
// release.prefix). The chart's templates/mariadb.yaml then materialises
|
|
// the operator-side k8s.mariadb.com/MariaDB CR with metadata.name set to
|
|
// .Release.Name, so the driver must look up by the prefixed name.
|
|
// Mirrors the CNPG driver's postgresAppPrefix / cnpgClusterNameForApp.
|
|
mariadbAppKind = "MariaDB"
|
|
mariadbAppPrefix = "mariadb-"
|
|
|
|
// Driver-metadata keys persisted on Cozystack Backup artifacts. Restore
|
|
// path reads these to drive the operator Restore CR.
|
|
mariadbBackupNameKey = "k8s.mariadb.com/backup-name"
|
|
mariadbBackupNamespaceKey = "k8s.mariadb.com/backup-namespace"
|
|
mariadbStorageURIKey = "k8s.mariadb.com/storage-uri"
|
|
|
|
// Polling cadence for the operator Backup/Restore lifecycle.
|
|
mariadbPollInterval = 5 * time.Second
|
|
|
|
// Wall-clock cap on a BackupJob waiting for the operator Backup to
|
|
// flip Complete. CNPG uses the same number for the same reason: a
|
|
// permanently-stuck Backup must not pin the BackupJob in Running and
|
|
// wedge the Plan-controller queue.
|
|
mariadbDefaultBackupDeadline = 30 * time.Minute
|
|
|
|
// Default deadline on a RestoreJob waiting for the operator Restore
|
|
// to terminate. Tenants override via spec.options.restoreTimeoutSeconds.
|
|
mariadbDefaultRestoreDeadline = 30 * time.Minute
|
|
|
|
// mariadbBackupSnapshotKind is the Kind stamped onto the snapshot
|
|
// persisted in Backup.status.underlyingResources. The same shape carries
|
|
// the storage descriptor and the rendered strategy parameters used at
|
|
// backup time, so restore-time re-rendering produces deterministic
|
|
// values.
|
|
mariadbBackupSnapshotKind = "MariaDBBackupSnapshot"
|
|
)
|
|
|
|
// mariadbBackupSnapshotAPIVersion is the apiVersion stamped onto the snapshot.
|
|
// Borrows the Cozystack backups group so the field is self-typed within the
|
|
// existing API surface.
|
|
var mariadbBackupSnapshotAPIVersion = backupsv1alpha1.GroupVersion.String()
|
|
|
|
// mariadbNameForApp returns the k8s.mariadb.com/MariaDB CR name for a
|
|
// cozystack MariaDB application instance. The mapping mirrors the mariadb
|
|
// ApplicationDefinition (release.prefix=mariadb-).
|
|
func mariadbNameForApp(appName string) string {
|
|
return mariadbAppPrefix + appName
|
|
}
|
|
|
|
// validateMariaDBApplicationRef rejects ApplicationRefs that name a
|
|
// Kind/APIGroup the MariaDB driver does not own. The driver assumes
|
|
// apps.cozystack.io/MariaDB; without this gate a ref like
|
|
// other.example.com/MariaDB would be accepted by the Kind check alone and
|
|
// then reconciled against the wrong CRD via the typed client.
|
|
func validateMariaDBApplicationRef(ref corev1.TypedLocalObjectReference) error {
|
|
if ref.Kind != mariadbAppKind {
|
|
return fmt.Errorf("MariaDB strategy supports applicationRef.kind=%q, got %q", mariadbAppKind, ref.Kind)
|
|
}
|
|
apiGroup := ""
|
|
if ref.APIGroup != nil {
|
|
apiGroup = *ref.APIGroup
|
|
}
|
|
if apiGroup != "" && apiGroup != mariadbapp.GroupName {
|
|
return fmt.Errorf("MariaDB strategy supports applicationRef.apiGroup=%q, got %q", mariadbapp.GroupName, apiGroup)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// BackupJob path
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (r *BackupJobReconciler) reconcileMariaDB(ctx context.Context, j *backupsv1alpha1.BackupJob, resolved *ResolvedBackupConfig) (ctrl.Result, error) {
|
|
logger := getLogger(ctx)
|
|
logger.Debug("reconciling MariaDB strategy", "backupjob", j.Name, "phase", j.Status.Phase)
|
|
|
|
if j.Status.Phase == backupsv1alpha1.BackupJobPhaseSucceeded ||
|
|
j.Status.Phase == backupsv1alpha1.BackupJobPhaseFailed {
|
|
return ctrl.Result{}, nil
|
|
}
|
|
|
|
if err := validateMariaDBApplicationRef(j.Spec.ApplicationRef); err != nil {
|
|
return r.markBackupJobFailed(ctx, j, err.Error())
|
|
}
|
|
|
|
if j.Status.StartedAt == nil {
|
|
// Refetch the latest persisted state before writing StartedAt: a
|
|
// stale informer cache that returns StartedAt==nil after we already
|
|
// persisted it would otherwise let the deadline gate slide forward
|
|
// on every poll. Same idempotency pattern as the CNPG driver.
|
|
fresh := &backupsv1alpha1.BackupJob{}
|
|
if err := r.Get(ctx, types.NamespacedName{Namespace: j.Namespace, Name: j.Name}, fresh); err != nil {
|
|
return ctrl.Result{}, err
|
|
}
|
|
if fresh.Status.StartedAt != nil {
|
|
j.Status.StartedAt = fresh.Status.StartedAt
|
|
} else {
|
|
base := fresh.DeepCopy()
|
|
now := metav1.Now()
|
|
fresh.Status.StartedAt = &now
|
|
if err := r.Status().Patch(ctx, fresh, client.MergeFrom(base)); err != nil {
|
|
return ctrl.Result{}, err
|
|
}
|
|
return ctrl.Result{RequeueAfter: mariadbPollInterval}, nil
|
|
}
|
|
}
|
|
|
|
strategy := &strategyv1alpha1.MariaDB{}
|
|
if err := r.Get(ctx, client.ObjectKey{Name: resolved.StrategyRef.Name}, strategy); err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
return r.requeueStrategyNotReady(ctx, j, resolved.StrategyRef.Name)
|
|
}
|
|
return ctrl.Result{}, err
|
|
}
|
|
|
|
app, err := r.getMariaDBApp(ctx, j.Namespace, j.Spec.ApplicationRef.Name)
|
|
if err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
return r.markBackupJobFailed(ctx, j, fmt.Sprintf("MariaDB application not found: %s/%s", j.Namespace, j.Spec.ApplicationRef.Name))
|
|
}
|
|
return ctrl.Result{}, err
|
|
}
|
|
|
|
rendered, err := renderMariaDBTemplate(strategy.Spec.Template, app, resolved.Parameters)
|
|
if err != nil {
|
|
return r.markBackupJobFailed(ctx, j, fmt.Sprintf("failed to template MariaDB strategy: %v", err))
|
|
}
|
|
|
|
// Operator-side MariaDB CR carries the prefixed release name (see
|
|
// mariadbNameForApp); verify it exists before we ask the operator to
|
|
// back it up. NotFound is normally transient (the chart may still be
|
|
// rendering on a fresh app), but bounded by mariadbDefaultBackupDeadline
|
|
// — otherwise a BackupJob whose applicationRef.name typo never resolves
|
|
// would pin in Running forever. StartedAt was persisted at line 132,
|
|
// so the deadline clock already started ticking on the first reconcile.
|
|
mdbName := mariadbNameForApp(j.Spec.ApplicationRef.Name)
|
|
if err := r.Get(ctx, types.NamespacedName{Namespace: j.Namespace, Name: mdbName},
|
|
&mariadbtypes.MariaDB{}); err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
if mariadbBackupDeadlineExceeded(j.Status.StartedAt) {
|
|
return r.markBackupJobFailed(ctx, j, fmt.Sprintf(
|
|
"k8s.mariadb.com/MariaDB %s/%s never reached existence within %s",
|
|
j.Namespace, mdbName, mariadbDefaultBackupDeadline))
|
|
}
|
|
apimeta.SetStatusCondition(&j.Status.Conditions, metav1.Condition{
|
|
Type: "Ready",
|
|
Status: metav1.ConditionFalse,
|
|
Reason: "MariaDBNotReady",
|
|
Message: fmt.Sprintf("waiting for k8s.mariadb.com/MariaDB %s/%s to exist", j.Namespace, mdbName),
|
|
})
|
|
if err := r.Status().Update(ctx, j); err != nil {
|
|
return ctrl.Result{}, err
|
|
}
|
|
return ctrl.Result{RequeueAfter: mariadbPollInterval}, nil
|
|
}
|
|
return ctrl.Result{}, err
|
|
}
|
|
|
|
mdbBackup, err := r.ensureMariaDBBackup(ctx, j, rendered)
|
|
if err != nil {
|
|
return r.markBackupJobFailed(ctx, j, fmt.Sprintf("failed to ensure k8s.mariadb.com/Backup: %v", err))
|
|
}
|
|
|
|
if j.Status.Phase != backupsv1alpha1.BackupJobPhaseRunning {
|
|
j.Status.Phase = backupsv1alpha1.BackupJobPhaseRunning
|
|
if err := r.Status().Update(ctx, j); err != nil {
|
|
return ctrl.Result{}, err
|
|
}
|
|
}
|
|
|
|
cond := apimeta.FindStatusCondition(mdbBackup.Status.Conditions, mariadbtypes.ConditionTypeComplete)
|
|
|
|
switch {
|
|
case cond != nil && cond.Status == metav1.ConditionTrue:
|
|
// Backup completed successfully. Materialise the Cozystack artifact
|
|
// and finalise the BackupJob. Idempotent: if the artifact already
|
|
// exists (a previous reconcile created it and then raced on the
|
|
// status update), reuse the existing object.
|
|
if j.Status.BackupRef != nil {
|
|
return ctrl.Result{}, nil
|
|
}
|
|
artifact, err := r.createMariaDBBackupArtifact(ctx, j, resolved, mdbBackup, rendered)
|
|
if err != nil {
|
|
return r.markBackupJobFailed(ctx, j, fmt.Sprintf("failed to create Backup artifact: %v", err))
|
|
}
|
|
now := metav1.Now()
|
|
j.Status.BackupRef = &corev1.LocalObjectReference{Name: artifact.Name}
|
|
j.Status.CompletedAt = &now
|
|
j.Status.Phase = backupsv1alpha1.BackupJobPhaseSucceeded
|
|
apimeta.SetStatusCondition(&j.Status.Conditions, metav1.Condition{
|
|
Type: "Ready",
|
|
Status: metav1.ConditionTrue,
|
|
Reason: "BackupCompleted",
|
|
Message: "k8s.mariadb.com Backup completed",
|
|
})
|
|
if err := r.Status().Update(ctx, j); err != nil {
|
|
return ctrl.Result{}, err
|
|
}
|
|
return ctrl.Result{}, nil
|
|
|
|
case cond != nil && cond.Status == metav1.ConditionFalse && cond.Reason == mariadbtypes.ConditionReasonJobFailed:
|
|
// Terminal failure. The operator marks Complete=False/JobFailed only
|
|
// after the underlying Job exhausts its backoff budget, so we can
|
|
// fail the BackupJob immediately rather than waiting for the
|
|
// driver-side deadline.
|
|
message := cond.Message
|
|
if message == "" {
|
|
message = "k8s.mariadb.com Backup terminal failure"
|
|
}
|
|
return r.markBackupJobFailed(ctx, j, message)
|
|
|
|
default:
|
|
// Still running (Complete=False/JobRunning, Complete=False/JobNotComplete,
|
|
// JobSuspended, or no Complete condition yet). Apply a wall-clock
|
|
// deadline so a permanently-stuck Backup eventually fails the
|
|
// BackupJob instead of pinning it Running forever.
|
|
if mariadbBackupDeadlineExceeded(j.Status.StartedAt) {
|
|
detail := "no Complete condition observed"
|
|
if cond != nil {
|
|
detail = fmt.Sprintf("Complete=%s/%s", cond.Status, cond.Reason)
|
|
}
|
|
return r.markBackupJobFailed(ctx, j, fmt.Sprintf(
|
|
"k8s.mariadb.com Backup did not complete within %s (%s)", mariadbDefaultBackupDeadline, detail))
|
|
}
|
|
return ctrl.Result{RequeueAfter: mariadbPollInterval}, nil
|
|
}
|
|
}
|
|
|
|
// mariadbBackupDeadlineExceeded reports whether enough wall-clock time has
|
|
// elapsed since the BackupJob started that we should give up on a stuck
|
|
// operator Backup. Returns false when StartedAt is nil so the very first
|
|
// reconcile (which sets StartedAt) does not trip the gate.
|
|
func mariadbBackupDeadlineExceeded(startedAt *metav1.Time) bool {
|
|
if startedAt == nil {
|
|
return false
|
|
}
|
|
return time.Since(startedAt.Time) > mariadbDefaultBackupDeadline
|
|
}
|
|
|
|
// ensureMariaDBBackup creates a one-shot k8s.mariadb.com/Backup CR labelled
|
|
// with the BackupJob, or returns the existing one if a previous reconcile
|
|
// already created it. Idempotency relies on the OwningJob labels.
|
|
func (r *BackupJobReconciler) ensureMariaDBBackup(ctx context.Context, j *backupsv1alpha1.BackupJob, rendered *strategyv1alpha1.MariaDBTemplate) (*mariadbtypes.Backup, error) {
|
|
existing, err := r.findMariaDBBackupForJob(ctx, j)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if existing != nil {
|
|
return existing, nil
|
|
}
|
|
|
|
storage, err := buildMariaDBBackupStorage(rendered.Storage)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
obj := &mariadbtypes.Backup{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: j.Namespace,
|
|
GenerateName: fmt.Sprintf("%s-", j.Name),
|
|
Labels: map[string]string{
|
|
backupsv1alpha1.OwningJobNameLabel: j.Name,
|
|
backupsv1alpha1.OwningJobNamespaceLabel: j.Namespace,
|
|
},
|
|
},
|
|
Spec: mariadbtypes.BackupSpec{
|
|
MariaDBRef: mariadbtypes.MariaDBObjectRef{Name: mariadbNameForApp(j.Spec.ApplicationRef.Name)},
|
|
Storage: storage,
|
|
Databases: append([]string(nil), rendered.Databases...),
|
|
Compression: rendered.Compression,
|
|
LogLevel: rendered.LogLevel,
|
|
MaxRetention: rendered.MaxRetention.DeepCopy(),
|
|
// Keep the source's grant table out of the dump so a restore into a
|
|
// copy leaves the target's chart-managed users and root intact (see
|
|
// BackupSpec.IgnoreGlobalPriv).
|
|
IgnoreGlobalPriv: ptr.To(true),
|
|
},
|
|
}
|
|
|
|
if err := r.Create(ctx, obj); err != nil {
|
|
return nil, err
|
|
}
|
|
return obj, nil
|
|
}
|
|
|
|
// findMariaDBBackupForJob returns the k8s.mariadb.com/Backup labelled with
|
|
// the BackupJob's OwningJob{Name,Namespace}, if any. Returns (nil, nil)
|
|
// when no match is found.
|
|
//
|
|
// In steady state at most one Backup CR carries a given OwningJob label
|
|
// pair (ensureMariaDBBackup is idempotent by this label). The list path
|
|
// can in theory observe two if two controller replicas raced Create
|
|
// between the first list returning empty and the cache catching up. The
|
|
// driver picks list.Items[0] deterministically; the V(1) breadcrumb on
|
|
// duplicates exists so an operator triaging "wrong Backup got reused"
|
|
// has a log line to grep for instead of staring at silent reuse.
|
|
func (r *BackupJobReconciler) findMariaDBBackupForJob(ctx context.Context, j *backupsv1alpha1.BackupJob) (*mariadbtypes.Backup, error) {
|
|
list := &mariadbtypes.BackupList{}
|
|
if err := r.List(ctx, list,
|
|
client.InNamespace(j.Namespace),
|
|
client.MatchingLabels{
|
|
backupsv1alpha1.OwningJobNameLabel: j.Name,
|
|
backupsv1alpha1.OwningJobNamespaceLabel: j.Namespace,
|
|
},
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(list.Items) == 0 {
|
|
return nil, nil
|
|
}
|
|
if len(list.Items) > 1 {
|
|
names := make([]string, 0, len(list.Items))
|
|
for i := range list.Items {
|
|
names = append(names, list.Items[i].Name)
|
|
}
|
|
getLogger(ctx).Debug("multiple k8s.mariadb.com/Backup CRs match BackupJob OwningJob labels; reusing first",
|
|
"backupjob", j.Name, "namespace", j.Namespace, "matches", names, "picked", names[0])
|
|
}
|
|
return &list.Items[0], nil
|
|
}
|
|
|
|
// createMariaDBBackupArtifact materialises a Cozystack Backup resource
|
|
// carrying the metadata callers need to drive a future restore. The
|
|
// rendered storage descriptor plus the BackupClass parameters are persisted
|
|
// in status.underlyingResources so a future fallback path (see
|
|
// marshalMariaDBBackupSnapshot) can re-render them when the operator-side
|
|
// Backup CR has been reaped.
|
|
func (r *BackupJobReconciler) createMariaDBBackupArtifact(
|
|
ctx context.Context,
|
|
j *backupsv1alpha1.BackupJob,
|
|
resolved *ResolvedBackupConfig,
|
|
mdbBackup *mariadbtypes.Backup,
|
|
rendered *strategyv1alpha1.MariaDBTemplate,
|
|
) (*backupsv1alpha1.Backup, error) {
|
|
takenAt := metav1.Now()
|
|
if cond := apimeta.FindStatusCondition(mdbBackup.Status.Conditions, mariadbtypes.ConditionTypeComplete); cond != nil && !cond.LastTransitionTime.IsZero() {
|
|
takenAt = cond.LastTransitionTime
|
|
}
|
|
|
|
driverMD := map[string]string{
|
|
mariadbBackupNameKey: mdbBackup.Name,
|
|
mariadbBackupNamespaceKey: mdbBackup.Namespace,
|
|
}
|
|
if uri := mariadbBackupURI(rendered, mdbBackup); uri != "" {
|
|
driverMD[mariadbStorageURIKey] = uri
|
|
}
|
|
|
|
underlyingResources, err := marshalMariaDBBackupSnapshot(rendered, resolved.Parameters)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encode source snapshot for Backup.status.underlyingResources: %w", err)
|
|
}
|
|
|
|
status := backupsv1alpha1.BackupStatus{
|
|
Phase: backupsv1alpha1.BackupPhaseReady,
|
|
UnderlyingResources: underlyingResources,
|
|
}
|
|
// Status.Artifact carries a "this backup has an artefact at URI X"
|
|
// signal. PVC/Volume storage has no stable URI (the location is the
|
|
// operator's PVC ID), so an Artifact{URI: ""} is a worse signal than
|
|
// no Artifact at all - leave it nil instead.
|
|
if uri := driverMD[mariadbStorageURIKey]; uri != "" {
|
|
status.Artifact = &backupsv1alpha1.BackupArtifact{URI: uri}
|
|
}
|
|
backup := &backupsv1alpha1.Backup{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: j.Name,
|
|
Namespace: j.Namespace,
|
|
},
|
|
Spec: backupsv1alpha1.BackupSpec{
|
|
ApplicationRef: j.Spec.ApplicationRef,
|
|
StrategyRef: resolved.StrategyRef,
|
|
TakenAt: takenAt,
|
|
DriverMetadata: driverMD,
|
|
},
|
|
Status: status,
|
|
}
|
|
if j.Spec.PlanRef != nil {
|
|
backup.Spec.PlanRef = j.Spec.PlanRef
|
|
}
|
|
if err := r.Create(ctx, backup); err != nil {
|
|
if !apierrors.IsAlreadyExists(err) {
|
|
return nil, err
|
|
}
|
|
existing := &backupsv1alpha1.Backup{}
|
|
if getErr := r.Get(ctx, types.NamespacedName{Namespace: backup.Namespace, Name: backup.Name}, existing); getErr != nil {
|
|
return nil, getErr
|
|
}
|
|
return existing, nil
|
|
}
|
|
return backup, nil
|
|
}
|
|
|
|
// mariadbBackupURI synthesises a human-readable URI for the Cozystack
|
|
// Backup artifact. Empty when the storage shape doesn't resolve to a stable
|
|
// URI (PVC/Volume, where the location is the operator's PVC ID).
|
|
func mariadbBackupURI(rendered *strategyv1alpha1.MariaDBTemplate, mdbBackup *mariadbtypes.Backup) string {
|
|
if rendered.Storage.S3 == nil {
|
|
return ""
|
|
}
|
|
s := rendered.Storage.S3
|
|
prefix := s.Prefix
|
|
if prefix != "" && prefix[len(prefix)-1] != '/' {
|
|
prefix += "/"
|
|
}
|
|
return fmt.Sprintf("s3://%s/%s%s", s.Bucket, prefix, mdbBackup.Name)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// RestoreJob path
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (r *RestoreJobReconciler) reconcileMariaDBRestore(ctx context.Context, restoreJob *backupsv1alpha1.RestoreJob, backup *backupsv1alpha1.Backup) (ctrl.Result, error) {
|
|
logger := getLogger(ctx)
|
|
logger.Debug("reconciling MariaDB restore", "restorejob", restoreJob.Name, "backup", backup.Name)
|
|
|
|
if err := validateMariaDBApplicationRef(backup.Spec.ApplicationRef); err != nil {
|
|
return r.markRestoreJobFailed(ctx, restoreJob, err.Error())
|
|
}
|
|
|
|
// Validate the resolved target shape before any apiserver call. This is
|
|
// cheap (in-memory state only) and lets a malformed TargetApplicationRef
|
|
// fail fast instead of after parseMariaDBRestoreOptions + the srcBackup
|
|
// Get round-trip.
|
|
target := r.resolveMariaDBRestoreTarget(restoreJob, backup)
|
|
if target.Kind != mariadbAppKind {
|
|
return r.markRestoreJobFailed(ctx, restoreJob, fmt.Sprintf(
|
|
"target applicationRef.kind=%q is not supported by the MariaDB driver", target.Kind))
|
|
}
|
|
if target.APIGroup != "" && target.APIGroup != mariadbapp.GroupName {
|
|
return r.markRestoreJobFailed(ctx, restoreJob, fmt.Sprintf(
|
|
"target applicationRef.apiGroup=%q is not supported by the MariaDB driver", target.APIGroup))
|
|
}
|
|
|
|
if restoreJob.Status.StartedAt == nil {
|
|
// Refetch the latest persisted state before writing StartedAt; same
|
|
// stale-cache idempotency pattern the BackupJob path uses.
|
|
fresh := &backupsv1alpha1.RestoreJob{}
|
|
if err := r.Get(ctx, types.NamespacedName{Namespace: restoreJob.Namespace, Name: restoreJob.Name}, fresh); err != nil {
|
|
return ctrl.Result{}, err
|
|
}
|
|
if fresh.Status.StartedAt != nil {
|
|
// A previous reconcile (or another replica) already persisted
|
|
// StartedAt - adopt it locally and continue inline so we don't
|
|
// waste a reconcile cycle bouncing through the cache. Mirrors
|
|
// the BackupJob path above.
|
|
restoreJob.Status.StartedAt = fresh.Status.StartedAt
|
|
if fresh.Status.Phase != "" {
|
|
restoreJob.Status.Phase = fresh.Status.Phase
|
|
}
|
|
} else {
|
|
base := fresh.DeepCopy()
|
|
now := metav1.Now()
|
|
fresh.Status.StartedAt = &now
|
|
fresh.Status.Phase = backupsv1alpha1.RestoreJobPhaseRunning
|
|
if err := r.Status().Patch(ctx, fresh, client.MergeFrom(base)); err != nil {
|
|
return ctrl.Result{}, err
|
|
}
|
|
restoreJob.Status.StartedAt = fresh.Status.StartedAt
|
|
restoreJob.Status.Phase = fresh.Status.Phase
|
|
// Requeue so the patched status round-trips via the informer
|
|
// cache before subsequent reconciles read it back.
|
|
return ctrl.Result{RequeueAfter: mariadbPollInterval}, nil
|
|
}
|
|
}
|
|
|
|
// parseMariaDBRestoreOptions is intentionally permissive (event + log,
|
|
// proceed with defaults). This diverges from the structural failures
|
|
// below (missing driverMetadata, MariaDB CR genuinely absent) because a
|
|
// bad spec.options is best-effort tenant-supplied override, not a
|
|
// load-bearing piece of the restore contract - the defaults are still
|
|
// the right behaviour, so surfacing a transient breadcrumb beats
|
|
// failing the restore on a typo.
|
|
options, err := parseMariaDBRestoreOptions(restoreJob.Spec.Options)
|
|
if err != nil {
|
|
logger.Info("malformed restoreJob.spec.options; falling back to defaults", "error", err)
|
|
r.Recorder.Eventf(restoreJob, corev1.EventTypeWarning, "MalformedOptions",
|
|
"spec.options is not valid JSON; falling back to defaults: %v", err)
|
|
}
|
|
|
|
sourceBackupName := backup.Spec.DriverMetadata[mariadbBackupNameKey]
|
|
if sourceBackupName == "" {
|
|
return r.markRestoreJobFailed(ctx, restoreJob,
|
|
"Backup driverMetadata is missing k8s.mariadb.com/backup-name (re-take the backup with a controller version that persists the operator Backup name)")
|
|
}
|
|
|
|
// Verify the operator Backup CR still exists. The operator's Restore CR
|
|
// only resolves backupRef in the same namespace as the Restore itself,
|
|
// so cross-namespace restores aren't supported here. NotFound surfaces
|
|
// as a terminal failure with a clear migration path (re-run the
|
|
// BackupJob to materialise a fresh artifact).
|
|
srcBackup := &mariadbtypes.Backup{}
|
|
if err := r.Get(ctx, types.NamespacedName{Namespace: backup.Namespace, Name: sourceBackupName}, srcBackup); err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
return r.markRestoreJobFailed(ctx, restoreJob, fmt.Sprintf(
|
|
"k8s.mariadb.com/Backup %s/%s not found; the operator-side artifact has been reaped",
|
|
backup.Namespace, sourceBackupName))
|
|
}
|
|
return ctrl.Result{}, err
|
|
}
|
|
|
|
// The operator Restore CR replays the dump against an existing live
|
|
// MariaDB; for to-copy this requires the target MariaDB to exist. A
|
|
// tenant who fires a RestoreJob seconds before their target
|
|
// HelmRelease materialises the MariaDB CR shouldn't have to recreate
|
|
// the RestoreJob - mirror the backup path's transient handling and let
|
|
// the operator-side StartedAt + effectiveRestoreDeadline guard against
|
|
// a target that never shows up.
|
|
targetMDBName := mariadbNameForApp(target.AppName)
|
|
if err := r.Get(ctx, types.NamespacedName{Namespace: target.Namespace, Name: targetMDBName},
|
|
&mariadbtypes.MariaDB{}); err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
deadline := options.effectiveRestoreDeadline()
|
|
if restoreJob.Status.StartedAt != nil && time.Since(restoreJob.Status.StartedAt.Time) > deadline {
|
|
return r.markRestoreJobFailed(ctx, restoreJob, fmt.Sprintf(
|
|
"target k8s.mariadb.com/MariaDB %s/%s not found within %s (deploy the target MariaDB application before requesting the restore; override via spec.options.restoreTimeoutSeconds)",
|
|
target.Namespace, targetMDBName, deadline))
|
|
}
|
|
apimeta.SetStatusCondition(&restoreJob.Status.Conditions, metav1.Condition{
|
|
Type: "Ready",
|
|
Status: metav1.ConditionFalse,
|
|
Reason: "TargetMariaDBNotReady",
|
|
Message: fmt.Sprintf("waiting for target k8s.mariadb.com/MariaDB %s/%s to exist",
|
|
target.Namespace, targetMDBName),
|
|
})
|
|
if err := r.Status().Update(ctx, restoreJob); err != nil {
|
|
return ctrl.Result{}, err
|
|
}
|
|
return ctrl.Result{RequeueAfter: mariadbPollInterval}, nil
|
|
}
|
|
return ctrl.Result{}, err
|
|
}
|
|
|
|
mdbRestore, err := r.ensureMariaDBRestore(ctx, restoreJob, sourceBackupName, targetMDBName)
|
|
if err != nil {
|
|
return r.markRestoreJobFailed(ctx, restoreJob, fmt.Sprintf("failed to ensure k8s.mariadb.com/Restore: %v", err))
|
|
}
|
|
|
|
cond := apimeta.FindStatusCondition(mdbRestore.Status.Conditions, mariadbtypes.ConditionTypeComplete)
|
|
switch {
|
|
case cond != nil && cond.Status == metav1.ConditionTrue:
|
|
now := metav1.Now()
|
|
restoreJob.Status.CompletedAt = &now
|
|
restoreJob.Status.Phase = backupsv1alpha1.RestoreJobPhaseSucceeded
|
|
apimeta.SetStatusCondition(&restoreJob.Status.Conditions, metav1.Condition{
|
|
Type: "Ready",
|
|
Status: metav1.ConditionTrue,
|
|
Reason: "RestoreCompleted",
|
|
Message: fmt.Sprintf("k8s.mariadb.com/Restore %s/%s completed", mdbRestore.Namespace, mdbRestore.Name),
|
|
})
|
|
if err := r.Status().Update(ctx, restoreJob); err != nil {
|
|
return ctrl.Result{}, err
|
|
}
|
|
return ctrl.Result{}, nil
|
|
|
|
case cond != nil && cond.Status == metav1.ConditionFalse && cond.Reason == mariadbtypes.ConditionReasonJobFailed:
|
|
message := cond.Message
|
|
if message == "" {
|
|
message = fmt.Sprintf("k8s.mariadb.com/Restore %s/%s terminal failure", mdbRestore.Namespace, mdbRestore.Name)
|
|
}
|
|
return r.markRestoreJobFailed(ctx, restoreJob, message)
|
|
|
|
default:
|
|
deadline := options.effectiveRestoreDeadline()
|
|
if restoreJob.Status.StartedAt != nil && time.Since(restoreJob.Status.StartedAt.Time) > deadline {
|
|
detail := "no Complete condition observed"
|
|
if cond != nil {
|
|
detail = fmt.Sprintf("Complete=%s/%s", cond.Status, cond.Reason)
|
|
}
|
|
return r.markRestoreJobFailed(ctx, restoreJob, fmt.Sprintf(
|
|
"k8s.mariadb.com/Restore did not complete within %s (%s; override via spec.options.restoreTimeoutSeconds)",
|
|
deadline, detail))
|
|
}
|
|
return ctrl.Result{RequeueAfter: mariadbPollInterval}, nil
|
|
}
|
|
}
|
|
|
|
// ensureMariaDBRestore creates a k8s.mariadb.com/Restore CR labelled with
|
|
// the RestoreJob, or returns the existing one if a previous reconcile
|
|
// already created it.
|
|
func (r *RestoreJobReconciler) ensureMariaDBRestore(ctx context.Context, rj *backupsv1alpha1.RestoreJob, sourceBackupName, targetMariaDBName string) (*mariadbtypes.Restore, error) {
|
|
list := &mariadbtypes.RestoreList{}
|
|
if err := r.List(ctx, list,
|
|
client.InNamespace(rj.Namespace),
|
|
client.MatchingLabels{
|
|
backupsv1alpha1.OwningJobNameLabel: rj.Name,
|
|
backupsv1alpha1.OwningJobNamespaceLabel: rj.Namespace,
|
|
},
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(list.Items) > 0 {
|
|
return &list.Items[0], nil
|
|
}
|
|
|
|
obj := &mariadbtypes.Restore{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: rj.Namespace,
|
|
GenerateName: fmt.Sprintf("%s-", rj.Name),
|
|
Labels: map[string]string{
|
|
backupsv1alpha1.OwningJobNameLabel: rj.Name,
|
|
backupsv1alpha1.OwningJobNamespaceLabel: rj.Namespace,
|
|
},
|
|
},
|
|
Spec: mariadbtypes.RestoreSpec{
|
|
MariaDBRef: mariadbtypes.MariaDBObjectRef{Name: targetMariaDBName},
|
|
BackupRef: &mariadbtypes.BackupReference{
|
|
Kind: mariadbtypes.BackupKind,
|
|
Name: sourceBackupName,
|
|
},
|
|
},
|
|
}
|
|
if err := r.Create(ctx, obj); err != nil {
|
|
return nil, err
|
|
}
|
|
return obj, nil
|
|
}
|
|
|
|
// mariadbRestoreTarget captures the resolved target for a MariaDB restore.
|
|
// The reconcile path treats in-place and to-copy identically: both create a
|
|
// k8s.mariadb.com/Restore CR pointing at the named target MariaDB. Callers
|
|
// infer the mode from AppName != backup.Spec.ApplicationRef.Name.
|
|
type mariadbRestoreTarget struct {
|
|
Namespace string
|
|
AppName string
|
|
Kind string
|
|
APIGroup string
|
|
}
|
|
|
|
func (r *RestoreJobReconciler) resolveMariaDBRestoreTarget(restoreJob *backupsv1alpha1.RestoreJob, backup *backupsv1alpha1.Backup) mariadbRestoreTarget {
|
|
t := mariadbRestoreTarget{
|
|
Namespace: backup.Namespace,
|
|
AppName: backup.Spec.ApplicationRef.Name,
|
|
Kind: backup.Spec.ApplicationRef.Kind,
|
|
}
|
|
if backup.Spec.ApplicationRef.APIGroup != nil {
|
|
t.APIGroup = *backup.Spec.ApplicationRef.APIGroup
|
|
}
|
|
if restoreJob.Spec.TargetApplicationRef != nil {
|
|
if restoreJob.Spec.TargetApplicationRef.Name != "" {
|
|
t.AppName = restoreJob.Spec.TargetApplicationRef.Name
|
|
}
|
|
if restoreJob.Spec.TargetApplicationRef.Kind != "" {
|
|
t.Kind = restoreJob.Spec.TargetApplicationRef.Kind
|
|
}
|
|
if restoreJob.Spec.TargetApplicationRef.APIGroup != nil {
|
|
t.APIGroup = *restoreJob.Spec.TargetApplicationRef.APIGroup
|
|
}
|
|
}
|
|
return t
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Snapshot persisted on Cozystack Backup.status.underlyingResources
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// mariadbBackupSnapshot is the MariaDB-specific payload persisted in
|
|
// Backup.status.underlyingResources at backup time. Carries the rendered
|
|
// storage descriptor and BackupClass parameters. The current restore path
|
|
// resolves the operator-side k8s.mariadb.com/Backup CR directly and fails
|
|
// terminally when it has been reaped; this snapshot is reserved for a
|
|
// future fallback path that would re-materialise the operator Backup from
|
|
// the persisted state. Keep the shape stable so an older snapshot stays
|
|
// machine-readable when that fallback lands.
|
|
//
|
|
// SECRET-HANDLING CONTRACT (this is plaintext-readable to anyone with read
|
|
// access to backups.cozystack.io/Backups):
|
|
//
|
|
// - Storage carries only Secret REFERENCES (Name + Key) - the
|
|
// MariaDBS3Template / MariaDBSecretKeySelector schema has no field for
|
|
// a raw credential value, so the snapshot can never contain an access
|
|
// key or secret access key. The mariadb-operator dereferences these
|
|
// names at runtime against the apiserver's Secret cache.
|
|
// - Parameters lands here verbatim from BackupClassStrategy.parameters.
|
|
// Callers MUST NOT put credentials in Parameters (it is documented on
|
|
// MariaDBSpec.Template that those values are not credential-bearing);
|
|
// a tenant who templates `{{ .Parameters.secretAccessKey }}` into a
|
|
// string field would persist that value here plaintext. The schema's
|
|
// credential-by-reference shape is the enforcement; this comment is
|
|
// the contract.
|
|
type mariadbBackupSnapshot struct {
|
|
Kind string `json:"kind"`
|
|
APIVersion string `json:"apiVersion"`
|
|
Storage *strategyv1alpha1.MariaDBStorageTemplate `json:"storage,omitempty"`
|
|
Databases []string `json:"databases,omitempty"`
|
|
Parameters map[string]string `json:"parameters,omitempty"`
|
|
}
|
|
|
|
// marshalMariaDBBackupSnapshot encodes the snapshot for persistence on the
|
|
// Cozystack Backup. See mariadbBackupSnapshot godoc for the secret-handling
|
|
// contract: Storage carries only Secret references (Name + Key), never
|
|
// raw credential values; Parameters is persisted verbatim and is plaintext.
|
|
func marshalMariaDBBackupSnapshot(rendered *strategyv1alpha1.MariaDBTemplate, parameters map[string]string) (*runtime.RawExtension, error) {
|
|
storage := rendered.Storage.DeepCopy()
|
|
snap := mariadbBackupSnapshot{
|
|
Kind: mariadbBackupSnapshotKind,
|
|
APIVersion: mariadbBackupSnapshotAPIVersion,
|
|
Storage: storage,
|
|
Databases: append([]string(nil), rendered.Databases...),
|
|
Parameters: parameters,
|
|
}
|
|
raw, err := json.Marshal(snap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &runtime.RawExtension{Raw: raw}, nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// renderMariaDBTemplate templates the strategy template against a context
|
|
// containing the live application object and the BackupClass parameters.
|
|
// Reuses the same templating helper as the CNPG / Velero strategies.
|
|
func renderMariaDBTemplate(t strategyv1alpha1.MariaDBTemplate, app *mariadbapp.MariaDB, parameters map[string]string) (*strategyv1alpha1.MariaDBTemplate, error) {
|
|
appAsMap, err := toJSONMapMariaDB(app)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encode application for templating: %w", err)
|
|
}
|
|
templateContext := map[string]any{
|
|
"Application": appAsMap,
|
|
"Parameters": parameters,
|
|
}
|
|
return template.Template(&t, templateContext)
|
|
}
|
|
|
|
// toJSONMapMariaDB converts a typed object to a generic map via JSON tags
|
|
// so user-authored go-templates address fields by their JSON names (e.g.
|
|
// .Application.metadata.name). Mirrors the helper in the CNPG controller;
|
|
// scoped here to avoid cross-strategy import.
|
|
func toJSONMapMariaDB(obj any) (map[string]any, error) {
|
|
raw, err := json.Marshal(obj)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := map[string]any{}
|
|
if err := json.Unmarshal(raw, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// getMariaDBApp fetches the apps.cozystack.io MariaDB instance via the
|
|
// shared typed client. The MariaDB scheme is registered in main.go so the
|
|
// controller-runtime cache serves it directly.
|
|
func (r *BackupJobReconciler) getMariaDBApp(ctx context.Context, namespace, name string) (*mariadbapp.MariaDB, error) {
|
|
app := &mariadbapp.MariaDB{}
|
|
if err := r.Get(ctx, types.NamespacedName{Namespace: namespace, Name: name}, app); err != nil {
|
|
return nil, err
|
|
}
|
|
return app, nil
|
|
}
|
|
|
|
// buildMariaDBBackupStorage translates the typed strategy template into the
|
|
// k8s.mariadb.com/v1alpha1 Backup.spec.storage shape. Exactly one of S3, PVC
|
|
// or Volume must be set; the CRD's XValidation rule enforces this at
|
|
// admission, but a defence-in-depth check here keeps the driver's behaviour
|
|
// well-defined for any pre-existing strategy CR that pre-dates the
|
|
// admission rule (otherwise the switch below would silently pick whichever
|
|
// branch comes first in source order).
|
|
func buildMariaDBBackupStorage(in strategyv1alpha1.MariaDBStorageTemplate) (mariadbtypes.BackupStorage, error) {
|
|
set := 0
|
|
if in.S3 != nil {
|
|
set++
|
|
}
|
|
if in.PersistentVolumeClaim != nil {
|
|
set++
|
|
}
|
|
if in.Volume != nil {
|
|
set++
|
|
}
|
|
if set != 1 {
|
|
return mariadbtypes.BackupStorage{}, fmt.Errorf(
|
|
"strategy.spec.template.storage requires exactly one of s3, persistentVolumeClaim or volume; got %d", set)
|
|
}
|
|
switch {
|
|
case in.S3 != nil:
|
|
out := &mariadbtypes.S3Storage{
|
|
Bucket: in.S3.Bucket,
|
|
Endpoint: in.S3.Endpoint,
|
|
Prefix: in.S3.Prefix,
|
|
Region: in.S3.Region,
|
|
AccessKeyIdSecretKeyRef: mariadbtypes.SecretKeySelector{
|
|
Name: in.S3.AccessKeyIDSecretKeyRef.Name,
|
|
Key: in.S3.AccessKeyIDSecretKeyRef.Key,
|
|
},
|
|
SecretAccessKeySecretKeyRef: mariadbtypes.SecretKeySelector{
|
|
Name: in.S3.SecretAccessKeySecretKeyRef.Name,
|
|
Key: in.S3.SecretAccessKeySecretKeyRef.Key,
|
|
},
|
|
}
|
|
if in.S3.SessionTokenSecretKeyRef != nil {
|
|
out.SessionTokenSecretKeyRef = &mariadbtypes.SecretKeySelector{
|
|
Name: in.S3.SessionTokenSecretKeyRef.Name,
|
|
Key: in.S3.SessionTokenSecretKeyRef.Key,
|
|
}
|
|
}
|
|
if in.S3.TLS != nil {
|
|
tls := &mariadbtypes.S3TLS{Enabled: in.S3.TLS.Enabled}
|
|
if in.S3.TLS.CASecretKeyRef != nil {
|
|
tls.CASecretKeyRef = &mariadbtypes.SecretKeySelector{
|
|
Name: in.S3.TLS.CASecretKeyRef.Name,
|
|
Key: in.S3.TLS.CASecretKeyRef.Key,
|
|
}
|
|
}
|
|
out.TLS = tls
|
|
}
|
|
return mariadbtypes.BackupStorage{S3: out}, nil
|
|
case in.PersistentVolumeClaim != nil:
|
|
return mariadbtypes.BackupStorage{PersistentVolumeClaim: in.PersistentVolumeClaim.DeepCopy()}, nil
|
|
case in.Volume != nil:
|
|
return mariadbtypes.BackupStorage{Volume: in.Volume.DeepCopy()}, nil
|
|
default:
|
|
return mariadbtypes.BackupStorage{}, fmt.Errorf("strategy.spec.template.storage requires exactly one of s3, persistentVolumeClaim or volume")
|
|
}
|
|
}
|
|
|
|
// MariaDBRestoreOptions is the typed shape of RestoreJob.Spec.Options for
|
|
// the MariaDB driver. Mirrors the CNPG strategy's RestoreOptions pattern
|
|
// so the boundary parses lazily and keeps behaviour permissive.
|
|
type MariaDBRestoreOptions struct {
|
|
// RestoreTimeoutSeconds caps the time the driver waits for the
|
|
// k8s.mariadb.com/Restore to terminate before it marks the RestoreJob
|
|
// Failed. Zero or unset falls back to mariadbDefaultRestoreDeadline.
|
|
// +optional
|
|
RestoreTimeoutSeconds int64 `json:"restoreTimeoutSeconds,omitempty"`
|
|
}
|
|
|
|
func parseMariaDBRestoreOptions(opts *runtime.RawExtension) (MariaDBRestoreOptions, error) {
|
|
var out MariaDBRestoreOptions
|
|
if opts == nil || len(opts.Raw) == 0 {
|
|
return out, nil
|
|
}
|
|
if err := json.Unmarshal(opts.Raw, &out); err != nil {
|
|
return MariaDBRestoreOptions{}, fmt.Errorf("decode restoreJob.spec.options: %w", err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (o MariaDBRestoreOptions) effectiveRestoreDeadline() time.Duration {
|
|
if o.RestoreTimeoutSeconds > 0 {
|
|
return time.Duration(o.RestoreTimeoutSeconds) * time.Second
|
|
}
|
|
return mariadbDefaultRestoreDeadline
|
|
}
|