diff --git a/changelogs/unreleased/8464-shubham-pampattiwar b/changelogs/unreleased/8464-shubham-pampattiwar new file mode 100644 index 000000000..9ffeedbdf --- /dev/null +++ b/changelogs/unreleased/8464-shubham-pampattiwar @@ -0,0 +1 @@ +Allowing Object-Level Resource Status Restore \ No newline at end of file diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index 899f7adde..22c88f707 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -79,6 +79,8 @@ import ( "github.com/vmware-tanzu/velero/pkg/util/results" ) +const ObjectStatusRestoreAnnotationKey = "velero.io/restore-status" + var resourceMustHave = []string{ "datauploads.velero.io", } @@ -1657,15 +1659,16 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso } // determine whether to restore status according to original GR - shouldRestoreStatus := ctx.resourceStatusIncludesExcludes != nil && ctx.resourceStatusIncludesExcludes.ShouldInclude(groupResource.String()) + shouldRestoreStatus := determineRestoreStatus(obj, ctx.resourceStatusIncludesExcludes, groupResource.String(), ctx.log) + if shouldRestoreStatus && statusFieldErr != nil { err := fmt.Errorf("could not get status to be restored %s: %v", kube.NamespaceAndName(obj), statusFieldErr) ctx.log.Errorf(err.Error()) errs.Add(namespace, err) return warnings, errs, itemExists } - ctx.log.Debugf("status field for %s: exists: %v, should restore: %v", newGR, statusFieldExists, shouldRestoreStatus) - // if it should restore status, run a UpdateStatus + + // Proceed with status restoration if decided if statusFieldExists && shouldRestoreStatus { if err := unstructured.SetNestedField(obj.Object, objStatus, "status"); err != nil { ctx.log.Errorf("could not set status field %s: %v", kube.NamespaceAndName(obj), err) @@ -2543,3 +2546,51 @@ func (ctx *restoreContext) handleSkippedPVHasRetainPolicy( obj = resetVolumeBindingInfo(obj) return obj, nil } + +func determineRestoreStatus( + obj *unstructured.Unstructured, + resourceIncludesExcludes *collections.IncludesExcludes, + groupResource string, + log logrus.FieldLogger, +) bool { + var shouldRestoreStatus bool + + // Determine restore spec behavior + if resourceIncludesExcludes != nil { + shouldRestoreStatus = resourceIncludesExcludes.ShouldInclude(groupResource) + } + + // Retrieve annotations + annotations := obj.GetAnnotations() + + if annotations == nil { + log.Warnf("No annotations found for %s, using restore spec setting: %v", + kube.NamespaceAndName(obj), shouldRestoreStatus) + return shouldRestoreStatus + } + + // Check for object-level annotation + objectAnnotation, annotationExists := annotations[ObjectStatusRestoreAnnotationKey] + + if !annotationExists { + log.Debugf("No restore status-specific annotation found for %s, using restore spec setting: %v", + kube.NamespaceAndName(obj), shouldRestoreStatus) + return shouldRestoreStatus + } + + normalizedValue := strings.ToLower(strings.TrimSpace(objectAnnotation)) + switch normalizedValue { + case "true": + shouldRestoreStatus = true + case "false": + shouldRestoreStatus = false + default: + log.Warnf("Invalid annotation value '%s' on %s, using restore spec setting: %v", + objectAnnotation, kube.NamespaceAndName(obj), shouldRestoreStatus) + } + + log.Infof("Final status restore decision for %s: %v (annotation: %v, restore spec: %v)", + kube.NamespaceAndName(obj), shouldRestoreStatus, annotationExists, shouldRestoreStatus) + + return shouldRestoreStatus +} diff --git a/pkg/restore/restore_test.go b/pkg/restore/restore_test.go index bfa767e76..a28a8ba62 100644 --- a/pkg/restore/restore_test.go +++ b/pkg/restore/restore_test.go @@ -25,6 +25,9 @@ import ( "testing" "time" + "github.com/vmware-tanzu/velero/pkg/util/boolptr" + "github.com/vmware-tanzu/velero/pkg/util/collections" + snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -4139,3 +4142,101 @@ func TestHasSnapshotDataUpload(t *testing.T) { }) } } + +func TestDetermineRestoreStatus(t *testing.T) { + tests := []struct { + name string + annotations map[string]string + restoreSpecIncludes *bool + expectedDecision bool + }{ + { + name: "No annotation, fallback to restore spec", + annotations: nil, + restoreSpecIncludes: boolptr.True(), + expectedDecision: true, + }, + { + name: "No annotation, restore spec excludes", + annotations: nil, + restoreSpecIncludes: boolptr.False(), + expectedDecision: false, + }, + { + name: "Annotation explicitly set to true, restore spec is false", + annotations: map[string]string{ObjectStatusRestoreAnnotationKey: "true"}, + restoreSpecIncludes: boolptr.False(), + expectedDecision: true, + }, + { + name: "Annotation explicitly set to false, restore spec is true", + annotations: map[string]string{ObjectStatusRestoreAnnotationKey: "false"}, + restoreSpecIncludes: boolptr.True(), + expectedDecision: false, + }, + { + name: "Invalid annotation value, fallback to restore spec", + annotations: map[string]string{ObjectStatusRestoreAnnotationKey: "foo"}, + restoreSpecIncludes: boolptr.True(), + expectedDecision: true, + }, + { + name: "Empty annotation value, fallback to restore spec", + annotations: map[string]string{ObjectStatusRestoreAnnotationKey: ""}, + restoreSpecIncludes: boolptr.False(), + expectedDecision: false, + }, + { + name: "Mixed-case annotation value 'True' should be treated as true", + annotations: map[string]string{ObjectStatusRestoreAnnotationKey: "True"}, + restoreSpecIncludes: boolptr.True(), + expectedDecision: true, + }, + { + name: "Mixed-case annotation value 'FALSE' should be treated as false", + annotations: map[string]string{ObjectStatusRestoreAnnotationKey: "FALSE"}, + restoreSpecIncludes: boolptr.True(), + expectedDecision: false, + }, + { + name: "Nil IncludesExcludes, but annotation is 'true'", + annotations: map[string]string{ObjectStatusRestoreAnnotationKey: "true"}, + restoreSpecIncludes: nil, + expectedDecision: true, + }, + { + name: "Nil IncludesExcludes, but annotation is 'false'", + annotations: map[string]string{ObjectStatusRestoreAnnotationKey: "false"}, + restoreSpecIncludes: nil, + expectedDecision: false, + }, + { + name: "Nil IncludesExcludes, no annotation (default to false)", + annotations: nil, + restoreSpecIncludes: nil, + expectedDecision: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + obj := &unstructured.Unstructured{} + obj.SetAnnotations(test.annotations) + + var includesExcludes *collections.IncludesExcludes + if test.restoreSpecIncludes != nil { + includesExcludes = collections.NewIncludesExcludes() + if *test.restoreSpecIncludes { + includesExcludes.Includes("*") + } else { + includesExcludes.Excludes("*") + } + } + + log := logrus.New() + result := determineRestoreStatus(obj, includesExcludes, "testGroupResource", log) + + assert.Equal(t, test.expectedDecision, result) + }) + } +}