[backups]
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
This commit is contained in:
parent
3eaadfc95c
commit
000b5ff76c
17 changed files with 481 additions and 488 deletions
|
|
@ -55,8 +55,9 @@ type VeleroSpec struct {
|
|||
// VeleroTemplate describes the data a backup.velero.io should have when
|
||||
// templated from a Velero backup strategy.
|
||||
type VeleroTemplate struct {
|
||||
Spec velerov1.BackupSpec `json:"spec"`
|
||||
RestoreSpec velerov1.RestoreSpec `json:"restoreSpec"`
|
||||
Spec velerov1.BackupSpec `json:"spec"`
|
||||
// +optional
|
||||
RestoreSpec *velerov1.RestoreSpec `json:"restoreSpec,omitempty"`
|
||||
}
|
||||
|
||||
type VeleroStatus struct {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ limitations under the License.
|
|||
package v1alpha1
|
||||
|
||||
import (
|
||||
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
|
@ -223,6 +224,11 @@ func (in *VeleroStatus) DeepCopy() *VeleroStatus {
|
|||
func (in *VeleroTemplate) DeepCopyInto(out *VeleroTemplate) {
|
||||
*out = *in
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
if in.RestoreSpec != nil {
|
||||
in, out := &in.RestoreSpec, &out.RestoreSpec
|
||||
*out = new(velerov1.RestoreSpec)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroTemplate.
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ type BackupJobSpec struct {
|
|||
// The BackupClass will be resolved to determine the appropriate strategy and storage
|
||||
// based on the ApplicationRef.
|
||||
// This field is immutable once the BackupJob is created.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="backupClassName is immutable"
|
||||
BackupClassName string `json:"backupClassName"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
)
|
||||
|
||||
// SetupWebhookWithManager registers the BackupJob webhook with the manager.
|
||||
func SetupBackupJobWebhookWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewWebhookManagedBy(mgr).
|
||||
For(&BackupJob{}).
|
||||
Complete()
|
||||
}
|
||||
|
||||
// +kubebuilder:webhook:path=/mutate-backups-cozystack-io-v1alpha1-backupjob,mutating=true,failurePolicy=fail,sideEffects=None,groups=backups.cozystack.io,resources=backupjobs,verbs=create;update,versions=v1alpha1,name=mbackupjob.kb.io,admissionReviewVersions=v1
|
||||
|
||||
// Default implements webhook.Defaulter so a webhook will be registered for the type
|
||||
func (j *BackupJob) Default() {
|
||||
j.Spec.ApplicationRef = NormalizeApplicationRef(j.Spec.ApplicationRef)
|
||||
}
|
||||
|
||||
// +kubebuilder:webhook:path=/validate-backups-cozystack-io-v1alpha1-backupjob,mutating=false,failurePolicy=fail,sideEffects=None,groups=backups.cozystack.io,resources=backupjobs,verbs=create;update,versions=v1alpha1,name=vbackupjob.kb.io,admissionReviewVersions=v1
|
||||
|
||||
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
|
||||
func (j *BackupJob) ValidateCreate() (admission.Warnings, error) {
|
||||
logger := log.FromContext(context.Background())
|
||||
logger.Info("validating BackupJob creation", "name", j.Name, "namespace", j.Namespace)
|
||||
|
||||
// Validate that backupClassName is set
|
||||
if strings.TrimSpace(j.Spec.BackupClassName) == "" {
|
||||
return nil, fmt.Errorf("backupClassName is required and cannot be empty")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
|
||||
func (j *BackupJob) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
|
||||
logger := log.FromContext(context.Background())
|
||||
logger.Info("validating BackupJob update", "name", j.Name, "namespace", j.Namespace)
|
||||
|
||||
oldJob, ok := old.(*BackupJob)
|
||||
if !ok {
|
||||
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a BackupJob but got a %T", old))
|
||||
}
|
||||
|
||||
// Enforce immutability of backupClassName
|
||||
if oldJob.Spec.BackupClassName != j.Spec.BackupClassName {
|
||||
return nil, fmt.Errorf("backupClassName is immutable and cannot be changed from %q to %q", oldJob.Spec.BackupClassName, j.Spec.BackupClassName)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
|
||||
func (j *BackupJob) ValidateDelete() (admission.Warnings, error) {
|
||||
// No validation needed for deletion
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -1,334 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func TestBackupJob_ValidateCreate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
job *BackupJob
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid BackupJob with backupClassName",
|
||||
job: &BackupJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-job",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: BackupJobSpec{
|
||||
ApplicationRef: corev1.TypedLocalObjectReference{
|
||||
Kind: "VirtualMachine",
|
||||
Name: "vm1",
|
||||
},
|
||||
BackupClassName: "velero",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "BackupJob with empty backupClassName should be rejected",
|
||||
job: &BackupJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-job",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: BackupJobSpec{
|
||||
ApplicationRef: corev1.TypedLocalObjectReference{
|
||||
Kind: "VirtualMachine",
|
||||
Name: "vm1",
|
||||
},
|
||||
BackupClassName: "",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "backupClassName is required and cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "BackupJob with whitespace-only backupClassName should be rejected",
|
||||
job: &BackupJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-job",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: BackupJobSpec{
|
||||
ApplicationRef: corev1.TypedLocalObjectReference{
|
||||
Kind: "VirtualMachine",
|
||||
Name: "vm1",
|
||||
},
|
||||
BackupClassName: " ",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "backupClassName is required and cannot be empty",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
warnings, err := tt.job.ValidateCreate()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateCreate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if tt.wantErr && err != nil {
|
||||
if tt.errMsg != "" && err.Error() != tt.errMsg {
|
||||
t.Errorf("ValidateCreate() error message = %v, want %v", err.Error(), tt.errMsg)
|
||||
}
|
||||
}
|
||||
if warnings != nil && len(warnings) > 0 {
|
||||
t.Logf("ValidateCreate() warnings = %v", warnings)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackupJob_ValidateUpdate(t *testing.T) {
|
||||
baseJob := &BackupJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-job",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: BackupJobSpec{
|
||||
ApplicationRef: corev1.TypedLocalObjectReference{
|
||||
Kind: "VirtualMachine",
|
||||
Name: "vm1",
|
||||
},
|
||||
BackupClassName: "velero",
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
old runtime.Object
|
||||
new *BackupJob
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "update with same backupClassName should succeed",
|
||||
old: baseJob,
|
||||
new: &BackupJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-job",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: BackupJobSpec{
|
||||
ApplicationRef: corev1.TypedLocalObjectReference{
|
||||
Kind: "VirtualMachine",
|
||||
Name: "vm1",
|
||||
},
|
||||
BackupClassName: "velero", // Same as old
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "update changing backupClassName should be rejected",
|
||||
old: baseJob,
|
||||
new: &BackupJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-job",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: BackupJobSpec{
|
||||
ApplicationRef: corev1.TypedLocalObjectReference{
|
||||
Kind: "VirtualMachine",
|
||||
Name: "vm1",
|
||||
},
|
||||
BackupClassName: "different-class", // Changed!
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "backupClassName is immutable and cannot be changed from \"velero\" to \"different-class\"",
|
||||
},
|
||||
{
|
||||
name: "update changing other fields but keeping backupClassName should succeed",
|
||||
old: baseJob,
|
||||
new: &BackupJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-job",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"new-label": "value",
|
||||
},
|
||||
},
|
||||
Spec: BackupJobSpec{
|
||||
ApplicationRef: corev1.TypedLocalObjectReference{
|
||||
Kind: "VirtualMachine",
|
||||
Name: "vm2", // Changed application
|
||||
},
|
||||
BackupClassName: "velero", // Same as old
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "update when old backupClassName is empty should be rejected",
|
||||
old: &BackupJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-job",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: BackupJobSpec{
|
||||
ApplicationRef: corev1.TypedLocalObjectReference{
|
||||
Kind: "VirtualMachine",
|
||||
Name: "vm1",
|
||||
},
|
||||
BackupClassName: "", // Empty in old
|
||||
},
|
||||
},
|
||||
new: &BackupJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-job",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: BackupJobSpec{
|
||||
ApplicationRef: corev1.TypedLocalObjectReference{
|
||||
Kind: "VirtualMachine",
|
||||
Name: "vm1",
|
||||
},
|
||||
BackupClassName: "velero", // Setting it for the first time
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "backupClassName is immutable",
|
||||
},
|
||||
{
|
||||
name: "update changing from non-empty to different non-empty should be rejected",
|
||||
old: &BackupJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-job",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: BackupJobSpec{
|
||||
ApplicationRef: corev1.TypedLocalObjectReference{
|
||||
Kind: "VirtualMachine",
|
||||
Name: "vm1",
|
||||
},
|
||||
BackupClassName: "class-a",
|
||||
},
|
||||
},
|
||||
new: &BackupJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-job",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: BackupJobSpec{
|
||||
ApplicationRef: corev1.TypedLocalObjectReference{
|
||||
Kind: "VirtualMachine",
|
||||
Name: "vm1",
|
||||
},
|
||||
BackupClassName: "class-b", // Changed from class-a
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "backupClassName is immutable and cannot be changed from \"class-a\" to \"class-b\"",
|
||||
},
|
||||
{
|
||||
name: "update with invalid old object type should be rejected",
|
||||
old: &corev1.Pod{ // Wrong type - will be cast to runtime.Object in test
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-job",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
new: baseJob,
|
||||
wantErr: true,
|
||||
errMsg: "expected a BackupJob but got a",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
warnings, err := tt.new.ValidateUpdate(tt.old)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateUpdate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
if err != nil {
|
||||
t.Logf("Error message: %v", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
if tt.wantErr && err != nil {
|
||||
if tt.errMsg != "" {
|
||||
if tt.errMsg != "" && !contains(err.Error(), tt.errMsg) {
|
||||
t.Errorf("ValidateUpdate() error message = %v, want contains %v", err.Error(), tt.errMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
if warnings != nil && len(warnings) > 0 {
|
||||
t.Logf("ValidateUpdate() warnings = %v", warnings)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackupJob_ValidateDelete(t *testing.T) {
|
||||
job := &BackupJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-job",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: BackupJobSpec{
|
||||
ApplicationRef: corev1.TypedLocalObjectReference{
|
||||
Kind: "VirtualMachine",
|
||||
Name: "vm1",
|
||||
},
|
||||
BackupClassName: "velero",
|
||||
},
|
||||
}
|
||||
|
||||
warnings, err := job.ValidateDelete()
|
||||
if err != nil {
|
||||
t.Errorf("ValidateDelete() should never return an error, got %v", err)
|
||||
}
|
||||
if warnings != nil && len(warnings) > 0 {
|
||||
t.Logf("ValidateDelete() warnings = %v", warnings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackupJob_Default(t *testing.T) {
|
||||
job := &BackupJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-job",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: BackupJobSpec{
|
||||
ApplicationRef: corev1.TypedLocalObjectReference{
|
||||
Kind: "VirtualMachine",
|
||||
Name: "vm1",
|
||||
},
|
||||
BackupClassName: "velero",
|
||||
},
|
||||
}
|
||||
|
||||
// Default() should not panic and should not modify the object
|
||||
originalClassName := job.Spec.BackupClassName
|
||||
job.Default()
|
||||
if job.Spec.BackupClassName != originalClassName {
|
||||
t.Errorf("Default() should not modify backupClassName, got %v, want %v", job.Spec.BackupClassName, originalClassName)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if a string contains a substring
|
||||
func contains(s, substr string) bool {
|
||||
if len(substr) == 0 {
|
||||
return true
|
||||
}
|
||||
if len(s) < len(substr) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -162,21 +162,6 @@ func main() {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&backupcontroller.BackupJobReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
Recorder: mgr.GetEventRecorderFor("backup-controller"),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "BackupJob")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Register BackupJob webhook for validation (immutability of backupClassName)
|
||||
if err = backupsv1alpha1.SetupBackupJobWebhookWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create webhook", "webhook", "BackupJob")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// +kubebuilder:scaffold:builder
|
||||
|
||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ set -o nounset
|
|||
set -o pipefail
|
||||
|
||||
SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/..
|
||||
CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)}
|
||||
CODEGEN_PKG=${CODEGEN_PKG:-~/go/pkg/mod/k8s.io/code-generator@v0.34.1}
|
||||
API_KNOWN_VIOLATIONS_DIR="${API_KNOWN_VIOLATIONS_DIR:-"${SCRIPT_ROOT}/api/api-rules"}"
|
||||
UPDATE_API_KNOWN_VIOLATIONS="${UPDATE_API_KNOWN_VIOLATIONS:-true}"
|
||||
CONTROLLER_GEN="go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.16.4"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package backupcontroller
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
|
|
@ -22,8 +21,8 @@ import (
|
|||
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
|
||||
)
|
||||
|
||||
// BackupVeleroStrategyReconciler reconciles BackupJob with a strategy referencing
|
||||
// Velero.strategy.backups.cozystack.io objects.
|
||||
// BackupJobReconciler reconciles BackupJob with a strategy from the
|
||||
// strategy.backups.cozystack.io API group.
|
||||
type BackupJobReconciler struct {
|
||||
client.Client
|
||||
dynamic.Interface
|
||||
|
|
|
|||
|
|
@ -106,9 +106,10 @@ func (r *RestoreJobReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
|||
// getTargetApplicationRef determines the effective target application reference.
|
||||
// According to DESIGN.md, if spec.targetApplicationRef is omitted, drivers SHOULD
|
||||
// restore into backup.spec.applicationRef.
|
||||
// The returned reference is normalized to ensure APIGroup has a default value.
|
||||
func (r *RestoreJobReconciler) getTargetApplicationRef(restoreJob *backupsv1alpha1.RestoreJob, backup *backupsv1alpha1.Backup) corev1.TypedLocalObjectReference {
|
||||
if restoreJob.Spec.TargetApplicationRef != nil {
|
||||
return *restoreJob.Spec.TargetApplicationRef
|
||||
return backupsv1alpha1.NormalizeApplicationRef(*restoreJob.Spec.TargetApplicationRef)
|
||||
}
|
||||
return backup.Spec.ApplicationRef
|
||||
}
|
||||
|
|
|
|||
|
|
@ -208,30 +208,6 @@ func (r *BackupJobReconciler) reconcileVelero(ctx context.Context, j *backupsv1a
|
|||
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
|
||||
}
|
||||
|
||||
func (r *BackupJobReconciler) markBackupJobFailed(ctx context.Context, backupJob *backupsv1alpha1.BackupJob, message string) (ctrl.Result, error) {
|
||||
logger := getLogger(ctx)
|
||||
now := metav1.Now()
|
||||
backupJob.Status.CompletedAt = &now
|
||||
backupJob.Status.Phase = backupsv1alpha1.BackupJobPhaseFailed
|
||||
backupJob.Status.Message = message
|
||||
|
||||
// Add condition
|
||||
backupJob.Status.Conditions = append(backupJob.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "BackupFailed",
|
||||
Message: message,
|
||||
LastTransitionTime: now,
|
||||
})
|
||||
|
||||
if err := r.Status().Update(ctx, backupJob); err != nil {
|
||||
logger.Error(err, "failed to update BackupJob status to Failed")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Debug("BackupJob failed", "message", message)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *BackupJobReconciler) createVeleroBackup(ctx context.Context, backupJob *backupsv1alpha1.BackupJob, strategy *strategyv1alpha1.Velero, resolved *ResolvedBackupConfig) error {
|
||||
logger := getLogger(ctx)
|
||||
logger.Debug("createVeleroBackup called", "strategy", strategy.Name)
|
||||
|
|
@ -476,39 +452,48 @@ func (r *RestoreJobReconciler) createVeleroRestore(ctx context.Context, restoreJ
|
|||
return fmt.Errorf("failed to get target application: %w", err)
|
||||
}
|
||||
|
||||
// Template the restore spec from the strategy
|
||||
veleroRestoreSpec, err := template.Template(&strategy.Spec.Template.RestoreSpec, app.Object)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to template Velero Restore spec: %w", err)
|
||||
// Build template context
|
||||
templateContext := map[string]interface{}{
|
||||
"Application": app.Object,
|
||||
// TODO: Parameters are not currently stored on Backup, so they're unavailable during restore.
|
||||
// This is a design limitation that should be addressed by persisting Parameters on the Backup object.
|
||||
"Parameters": map[string]string{},
|
||||
}
|
||||
|
||||
// Template the restore spec from the strategy, or use defaults if not specified
|
||||
var veleroRestoreSpec velerov1.RestoreSpec
|
||||
if strategy.Spec.Template.RestoreSpec != nil {
|
||||
templatedSpec, err := template.Template(strategy.Spec.Template.RestoreSpec, templateContext)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to template Velero Restore spec: %w", err)
|
||||
}
|
||||
veleroRestoreSpec = *templatedSpec
|
||||
}
|
||||
|
||||
// Set the backupName in the spec (required by Velero)
|
||||
veleroRestoreSpec.BackupName = veleroBackupName
|
||||
|
||||
generateName := fmt.Sprintf("%s.%s-", restoreJob.Namespace, restoreJob.Name)
|
||||
veleroRestore := &velerov1.Restore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: fmt.Sprintf("%s.%s-", restoreJob.Namespace, restoreJob.Name),
|
||||
GenerateName: generateName,
|
||||
Namespace: veleroNamespace,
|
||||
Labels: map[string]string{
|
||||
backupsv1alpha1.OwningJobNameLabel: restoreJob.Name,
|
||||
backupsv1alpha1.OwningJobNamespaceLabel: restoreJob.Namespace,
|
||||
},
|
||||
},
|
||||
Spec: *veleroRestoreSpec,
|
||||
Spec: veleroRestoreSpec,
|
||||
}
|
||||
name := veleroRestore.GenerateName
|
||||
if err := r.Create(ctx, veleroRestore); err != nil {
|
||||
if veleroRestore.Name != "" {
|
||||
name = veleroRestore.Name
|
||||
}
|
||||
logger.Error(err, "failed to create Velero Restore", "name", veleroRestore.Name)
|
||||
logger.Error(err, "failed to create Velero Restore", "generateName", generateName)
|
||||
r.Recorder.Event(restoreJob, corev1.EventTypeWarning, "VeleroRestoreCreationFailed",
|
||||
fmt.Sprintf("Failed to create Velero Restore %s/%s: %v", veleroNamespace, name, err))
|
||||
fmt.Sprintf("Failed to create Velero Restore %s/%s: %v", veleroNamespace, generateName, err))
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Debug("created Velero Restore", "name", veleroRestore.Name, "namespace", veleroRestore.Namespace)
|
||||
r.Recorder.Event(restoreJob, corev1.EventTypeNormal, "VeleroRestoreCreated",
|
||||
fmt.Sprintf("Created Velero Restore %s/%s", veleroNamespace, name))
|
||||
fmt.Sprintf("Created Velero Restore %s/%s", veleroNamespace, veleroRestore.Name))
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,12 @@ spec:
|
|||
BackupClassName references a BackupClass that contains strategy and storage configuration.
|
||||
The BackupClass will be resolved to determine the appropriate strategy and storage
|
||||
based on the ApplicationRef.
|
||||
This field is immutable once the BackupJob is created.
|
||||
minLength: 1
|
||||
type: string
|
||||
x-kubernetes-validations:
|
||||
- message: backupClassName is immutable
|
||||
rule: self == oldSelf
|
||||
planRef:
|
||||
description: |-
|
||||
PlanRef refers to the Plan that requested this backup run.
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@ spec:
|
|||
singular: restorejob
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .status.phase
|
||||
name: Phase
|
||||
type: string
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: RestoreJob represents a single execution of a restore from a
|
||||
|
|
@ -166,3 +170,5 @@ spec:
|
|||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
kind: BackupClass
|
||||
metadata:
|
||||
name: velero
|
||||
spec:
|
||||
strategies:
|
||||
- strategyRef:
|
||||
apiGroup: strategy.backups.cozystack.io
|
||||
kind: Velero
|
||||
name: velero-backup-strategy-for-virtualmachines
|
||||
application:
|
||||
apiVersion: apps.cozystack.io
|
||||
kind: VirtualMachine
|
||||
parameters:
|
||||
backupStorageLocationName: default
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
apiVersion: strategy.backups.cozystack.io/v1alpha1
|
||||
kind: Velero
|
||||
metadata:
|
||||
name: velero-strategy-default
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
ttl: 720h
|
||||
includedNamespaces:
|
||||
- "*"
|
||||
|
|
@ -45,6 +45,428 @@ spec:
|
|||
VeleroTemplate describes the data a backup.velero.io should have when
|
||||
templated from a Velero backup strategy.
|
||||
properties:
|
||||
restoreSpec:
|
||||
description: RestoreSpec defines the specification for a Velero
|
||||
restore.
|
||||
properties:
|
||||
backupName:
|
||||
description: |-
|
||||
BackupName is the unique name of the Velero backup to restore
|
||||
from.
|
||||
type: string
|
||||
excludedNamespaces:
|
||||
description: |-
|
||||
ExcludedNamespaces contains a list of namespaces that are not
|
||||
included in the restore.
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
excludedResources:
|
||||
description: |-
|
||||
ExcludedResources is a slice of resource names that are not
|
||||
included in the restore.
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
existingResourcePolicy:
|
||||
description: ExistingResourcePolicy specifies the restore
|
||||
behavior for the Kubernetes resource to be restored
|
||||
nullable: true
|
||||
type: string
|
||||
hooks:
|
||||
description: Hooks represent custom behaviors that should
|
||||
be executed during or post restore.
|
||||
properties:
|
||||
resources:
|
||||
items:
|
||||
description: |-
|
||||
RestoreResourceHookSpec defines one or more RestoreResrouceHooks that should be executed based on
|
||||
the rules defined for namespaces, resources, and label selector.
|
||||
properties:
|
||||
excludedNamespaces:
|
||||
description: ExcludedNamespaces specifies the namespaces
|
||||
to which this hook spec does not apply.
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
excludedResources:
|
||||
description: ExcludedResources specifies the resources
|
||||
to which this hook spec does not apply.
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
includedNamespaces:
|
||||
description: |-
|
||||
IncludedNamespaces specifies the namespaces to which this hook spec applies. If empty, it applies
|
||||
to all namespaces.
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
includedResources:
|
||||
description: |-
|
||||
IncludedResources specifies the resources to which this hook spec applies. If empty, it applies
|
||||
to all resources.
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
labelSelector:
|
||||
description: LabelSelector, if specified, filters
|
||||
the resources to which this hook spec applies.
|
||||
nullable: true
|
||||
properties:
|
||||
matchExpressions:
|
||||
description: matchExpressions is a list of label
|
||||
selector requirements. The requirements are
|
||||
ANDed.
|
||||
items:
|
||||
description: |-
|
||||
A label selector requirement is a selector that contains values, a key, and an operator that
|
||||
relates the key and values.
|
||||
properties:
|
||||
key:
|
||||
description: key is the label key that
|
||||
the selector applies to.
|
||||
type: string
|
||||
operator:
|
||||
description: |-
|
||||
operator represents a key's relationship to a set of values.
|
||||
Valid operators are In, NotIn, Exists and DoesNotExist.
|
||||
type: string
|
||||
values:
|
||||
description: |-
|
||||
values is an array of string values. If the operator is In or NotIn,
|
||||
the values array must be non-empty. If the operator is Exists or DoesNotExist,
|
||||
the values array must be empty. This array is replaced during a strategic
|
||||
merge patch.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
type: object
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
matchLabels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: |-
|
||||
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
|
||||
map is equivalent to an element of matchExpressions, whose key field is "key", the
|
||||
operator is "In", and the values array contains only "value". The requirements are ANDed.
|
||||
type: object
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
name:
|
||||
description: Name is the name of this hook.
|
||||
type: string
|
||||
postHooks:
|
||||
description: PostHooks is a list of RestoreResourceHooks
|
||||
to execute during and after restoring a resource.
|
||||
items:
|
||||
description: RestoreResourceHook defines a restore
|
||||
hook for a resource.
|
||||
properties:
|
||||
exec:
|
||||
description: Exec defines an exec restore
|
||||
hook.
|
||||
properties:
|
||||
command:
|
||||
description: Command is the command and
|
||||
arguments to execute from within a container
|
||||
after a pod has been restored.
|
||||
items:
|
||||
type: string
|
||||
minItems: 1
|
||||
type: array
|
||||
container:
|
||||
description: |-
|
||||
Container is the container in the pod where the command should be executed. If not specified,
|
||||
the pod's first container is used.
|
||||
type: string
|
||||
execTimeout:
|
||||
description: |-
|
||||
ExecTimeout defines the maximum amount of time Velero should wait for the hook to complete before
|
||||
considering the execution a failure.
|
||||
type: string
|
||||
onError:
|
||||
description: OnError specifies how Velero
|
||||
should behave if it encounters an error
|
||||
executing this hook.
|
||||
enum:
|
||||
- Continue
|
||||
- Fail
|
||||
type: string
|
||||
waitForReady:
|
||||
description: WaitForReady ensures command
|
||||
will be launched when container is Ready
|
||||
instead of Running.
|
||||
nullable: true
|
||||
type: boolean
|
||||
waitTimeout:
|
||||
description: |-
|
||||
WaitTimeout defines the maximum amount of time Velero should wait for the container to be Ready
|
||||
before attempting to run the command.
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
type: object
|
||||
init:
|
||||
description: Init defines an init restore
|
||||
hook.
|
||||
properties:
|
||||
initContainers:
|
||||
description: InitContainers is list of
|
||||
init containers to be added to a pod
|
||||
during its restore.
|
||||
items:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
type: array
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
timeout:
|
||||
description: Timeout defines the maximum
|
||||
amount of time Velero should wait for
|
||||
the initContainers to complete.
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: array
|
||||
type: object
|
||||
includeClusterResources:
|
||||
description: |-
|
||||
IncludeClusterResources specifies whether cluster-scoped resources
|
||||
should be included for consideration in the restore. If null, defaults
|
||||
to true.
|
||||
nullable: true
|
||||
type: boolean
|
||||
includedNamespaces:
|
||||
description: |-
|
||||
IncludedNamespaces is a slice of namespace names to include objects
|
||||
from. If empty, all namespaces are included.
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
includedResources:
|
||||
description: |-
|
||||
IncludedResources is a slice of resource names to include
|
||||
in the restore. If empty, all resources in the backup are included.
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
itemOperationTimeout:
|
||||
description: |-
|
||||
ItemOperationTimeout specifies the time used to wait for RestoreItemAction operations
|
||||
The default value is 4 hour.
|
||||
type: string
|
||||
labelSelector:
|
||||
description: |-
|
||||
LabelSelector is a metav1.LabelSelector to filter with
|
||||
when restoring individual objects from the backup. If empty
|
||||
or nil, all objects are included. Optional.
|
||||
nullable: true
|
||||
properties:
|
||||
matchExpressions:
|
||||
description: matchExpressions is a list of label selector
|
||||
requirements. The requirements are ANDed.
|
||||
items:
|
||||
description: |-
|
||||
A label selector requirement is a selector that contains values, a key, and an operator that
|
||||
relates the key and values.
|
||||
properties:
|
||||
key:
|
||||
description: key is the label key that the selector
|
||||
applies to.
|
||||
type: string
|
||||
operator:
|
||||
description: |-
|
||||
operator represents a key's relationship to a set of values.
|
||||
Valid operators are In, NotIn, Exists and DoesNotExist.
|
||||
type: string
|
||||
values:
|
||||
description: |-
|
||||
values is an array of string values. If the operator is In or NotIn,
|
||||
the values array must be non-empty. If the operator is Exists or DoesNotExist,
|
||||
the values array must be empty. This array is replaced during a strategic
|
||||
merge patch.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
type: object
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
matchLabels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: |-
|
||||
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
|
||||
map is equivalent to an element of matchExpressions, whose key field is "key", the
|
||||
operator is "In", and the values array contains only "value". The requirements are ANDed.
|
||||
type: object
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
namespaceMapping:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: |-
|
||||
NamespaceMapping is a map of source namespace names
|
||||
to target namespace names to restore into. Any source
|
||||
namespaces not included in the map will be restored into
|
||||
namespaces of the same name.
|
||||
type: object
|
||||
orLabelSelectors:
|
||||
description: |-
|
||||
OrLabelSelectors is list of metav1.LabelSelector to filter with
|
||||
when restoring individual objects from the backup. If multiple provided
|
||||
they will be joined by the OR operator. LabelSelector as well as
|
||||
OrLabelSelectors cannot co-exist in restore request, only one of them
|
||||
can be used
|
||||
items:
|
||||
description: |-
|
||||
A label selector is a label query over a set of resources. The result of matchLabels and
|
||||
matchExpressions are ANDed. An empty label selector matches all objects. A null
|
||||
label selector matches no objects.
|
||||
properties:
|
||||
matchExpressions:
|
||||
description: matchExpressions is a list of label selector
|
||||
requirements. The requirements are ANDed.
|
||||
items:
|
||||
description: |-
|
||||
A label selector requirement is a selector that contains values, a key, and an operator that
|
||||
relates the key and values.
|
||||
properties:
|
||||
key:
|
||||
description: key is the label key that the selector
|
||||
applies to.
|
||||
type: string
|
||||
operator:
|
||||
description: |-
|
||||
operator represents a key's relationship to a set of values.
|
||||
Valid operators are In, NotIn, Exists and DoesNotExist.
|
||||
type: string
|
||||
values:
|
||||
description: |-
|
||||
values is an array of string values. If the operator is In or NotIn,
|
||||
the values array must be non-empty. If the operator is Exists or DoesNotExist,
|
||||
the values array must be empty. This array is replaced during a strategic
|
||||
merge patch.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
type: object
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
matchLabels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: |-
|
||||
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
|
||||
map is equivalent to an element of matchExpressions, whose key field is "key", the
|
||||
operator is "In", and the values array contains only "value". The requirements are ANDed.
|
||||
type: object
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
nullable: true
|
||||
type: array
|
||||
preserveNodePorts:
|
||||
description: PreserveNodePorts specifies whether to restore
|
||||
old nodePorts from backup.
|
||||
nullable: true
|
||||
type: boolean
|
||||
resourceModifier:
|
||||
description: ResourceModifier specifies the reference to JSON
|
||||
resource patches that should be applied to resources before
|
||||
restoration.
|
||||
nullable: true
|
||||
properties:
|
||||
apiGroup:
|
||||
description: |-
|
||||
APIGroup is the group for the resource being referenced.
|
||||
If APIGroup is not specified, the specified Kind must be in the core API group.
|
||||
For any other third-party types, APIGroup is required.
|
||||
type: string
|
||||
kind:
|
||||
description: Kind is the type of resource being referenced
|
||||
type: string
|
||||
name:
|
||||
description: Name is the name of resource being referenced
|
||||
type: string
|
||||
required:
|
||||
- kind
|
||||
- name
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
restorePVs:
|
||||
description: |-
|
||||
RestorePVs specifies whether to restore all included
|
||||
PVs from snapshot
|
||||
nullable: true
|
||||
type: boolean
|
||||
restoreStatus:
|
||||
description: |-
|
||||
RestoreStatus specifies which resources we should restore the status
|
||||
field. If nil, no objects are included. Optional.
|
||||
nullable: true
|
||||
properties:
|
||||
excludedResources:
|
||||
description: ExcludedResources specifies the resources
|
||||
to which will not restore the status.
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
includedResources:
|
||||
description: |-
|
||||
IncludedResources specifies the resources to which will restore the status.
|
||||
If empty, it applies to all resources.
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
scheduleName:
|
||||
description: |-
|
||||
ScheduleName is the unique name of the Velero schedule to restore
|
||||
from. If specified, and BackupName is empty, Velero will restore
|
||||
from the most recent successful backup created from this schedule.
|
||||
type: string
|
||||
uploaderConfig:
|
||||
description: UploaderConfig specifies the configuration for
|
||||
the restore.
|
||||
nullable: true
|
||||
properties:
|
||||
parallelFilesDownload:
|
||||
description: ParallelFilesDownload is the concurrency
|
||||
number setting for restore.
|
||||
type: integer
|
||||
writeSparseFiles:
|
||||
description: WriteSparseFiles is a flag to indicate whether
|
||||
write files sparsely or not.
|
||||
nullable: true
|
||||
type: boolean
|
||||
type: object
|
||||
type: object
|
||||
spec:
|
||||
description: BackupSpec defines the specification for a Velero
|
||||
backup.
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ kind: Velero
|
|||
metadata:
|
||||
name: velero-backup-strategy-for-virtualmachines
|
||||
spec:
|
||||
backupTemplate:
|
||||
template:
|
||||
spec: # see https://velero.io/docs/v1.9/api-types/backup/
|
||||
includedNamespaces:
|
||||
- {{ .metadata.namespace }}
|
||||
- {{ .Application.metadata.namespace }}
|
||||
|
||||
labelSelector:
|
||||
apps.cozystack.io/application.Kind: {{ .kind }}
|
||||
apps.cozystack.io/application.Kind: {{ .Application.kind }}
|
||||
|
||||
includedResources:
|
||||
- helmreleases.helm.toolkit.fluxcd.io
|
||||
|
|
|
|||
|
|
@ -258,6 +258,13 @@ func schema_pkg_apis_apps_v1alpha1_ApplicationStatus(ref common.ReferenceCallbac
|
|||
Format: "",
|
||||
},
|
||||
},
|
||||
"externalIPsCount": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "ExternalIPsCount holds the number of LoadBalancer services with assigned external IPs for Tenant applications.",
|
||||
Type: []string{"integer"},
|
||||
Format: "int32",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue