Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation for the restore queue #128

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions api/v1alpha1/nonadminrestore_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ type NonAdminRestoreStatus struct {
// +optional
VeleroRestore *VeleroRestore `json:"veleroRestore,omitempty"`

// queueInfo is used to estimate how many restores are scheduled before the given VeleroRestore in the OADP namespace.
// This number is not guaranteed to be accurate, but it should be close. It's inaccurate for cases when
// Velero pod is not running or being restarted after Restore object were created.
// It counts only VeleroRestores that are still subject to be handled by OADP/Velero.
// +optional
QueueInfo *QueueInfo `json:"queueInfo,omitempty"`

// phase is a simple one high-level summary of the lifecycle of an NonAdminRestore.
Phase NonAdminPhase `json:"phase,omitempty"`

Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions config/crd/bases/oadp.openshift.io_nonadminrestores.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,20 @@ spec:
- Created
- Deleting
type: string
queueInfo:
description: |-
queueInfo is used to estimate how many restores are scheduled before the given VeleroRestore in the OADP namespace.
This number is not guaranteed to be accurate, but it should be close. It's inaccurate for cases when
Velero pod is not running or being restarted after Restore object were created.
It counts only VeleroRestores that are still subject to be handled by OADP/Velero.
properties:
estimatedQueuePosition:
description: estimatedQueuePosition is the number of operations
ahead in the queue (0 if not queued)
type: integer
required:
- estimatedQueuePosition
type: object
veleroRestore:
description: VeleroRestore contains information of the related Velero
restore object.
Expand Down
75 changes: 74 additions & 1 deletion internal/common/function/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,79 @@ func GetBackupQueueInfo(ctx context.Context, clientInstance client.Client, names
return queueInfo, nil
}

// GetActiveVeleroRestoresByLabel retrieves all VeleroRestore objects based on a specified label within a given namespace.
// It returns a slice of VeleroRestore objects or nil if none are found.
func GetActiveVeleroRestoresByLabel(ctx context.Context, clientInstance client.Client, namespace, labelKey, labelValue string) ([]velerov1.Restore, error) {
var veleroRestoreList velerov1.RestoreList
labelSelector := client.MatchingLabels{labelKey: labelValue}

if err := clientInstance.List(ctx, &veleroRestoreList, client.InNamespace(namespace), labelSelector); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what you want here is to get all NAC "owned" velero restores?

I suggest doing like this

func GetActiveOwnedVeleroRestores(ctx context.Context, clientInstance client.Client, namespace string) ([]velerov1.Restore, error) {
	var veleroRestoreList velerov1.RestoreList
	if err := ListObjectsByLabel(ctx, clientInstance, namespace, constant.ManagedByLabel, constant.ManagedByLabelValue, veleroRestoreList); err != nil {
		return nil, err
	}

	var activeRestores []velerov1.Restore
	for _, restore := range veleroRestoreList.Items {
		if restore.Status.CompletionTimestamp == nil && CheckVeleroRestoreMetadata(restore) {
			activeRestores = append(activeRestores, restore)
		}
	}

	if len(activeRestores) == 0 {
		return nil, nil
	}

	return activeRestores, nil
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want all the NAC owned restores that does not have restore.Status.CompletionTimestamp, which means they are still subject to be handled. We are making queue number based on the list of the objects that are still subject to some work by Velero, the ones that are completed (has the CompletionTimestamp) are not valid anymore.

Will change the implementation to use the ListObjectsByLabel.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also be changed? I don't want this to be any different? This PR/separate ?

func GetActiveVeleroBackupsByLabel(ctx context.Context, clientInstance client.Client, namespace, labelKey, labelValue string) ([]velerov1.Backup, error) {
var veleroBackupList velerov1.BackupList
labelSelector := client.MatchingLabels{labelKey: labelValue}
if err := clientInstance.List(ctx, &veleroBackupList, client.InNamespace(namespace), labelSelector); err != nil {
return nil, err
}
// Filter out backups with a CompletionTimestamp
var activeBackups []velerov1.Backup
for _, backup := range veleroBackupList.Items {
if backup.Status.CompletionTimestamp == nil {
activeBackups = append(activeBackups, backup)
}
}
if len(activeBackups) == 0 {
return nil, nil
}
return activeBackups, nil
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am ok doing change in this PR as well

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will leave as it is in this PR if you agree - changed the backup function and now the tests are failing showing always queue 0. I don't want to spent time in this PR to debug why. Current implementation works as expected.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is what I checked (even without checking metadata):

func GetActiveOwnedVeleroBackups(ctx context.Context, clientInstance client.Client, namespace string) ([]velerov1.Backup, error) {
	var veleroBackupList := &velerov1.BackupList{}

	if err := ListObjectsByLabel(ctx, clientInstance, namespace, constant.ManagedByLabel, constant.ManagedByLabelValue, veleroBackupList); err != nil {
		return nil, err
	}

	// Filter out backups with a CompletionTimestamp
	var activeBackups []velerov1.Backup
	for _, backup := range veleroBackupList.Items {
		if backup.Status.CompletionTimestamp == nil {
			activeBackups = append(activeBackups, backup)
		}
	}

	if len(activeBackups) == 0 {
		return nil, nil
	}

	return activeBackups, nil
}

return nil, err
}

// Filter out restores with a CompletionTimestamp
var activeRestores []velerov1.Restore
for _, restore := range veleroRestoreList.Items {
if restore.Status.CompletionTimestamp == nil {
activeRestores = append(activeRestores, restore)
}
}

if len(activeRestores) == 0 {
return nil, nil
}

return activeRestores, nil
}

// GetRestoreQueueInfo determines the queue position of the specified VeleroRestore.
// It calculates how many queued Restores exist in the namespace that were created before this one.
func GetRestoreQueueInfo(ctx context.Context, clientInstance client.Client, namespace string, targetRestore *velerov1.Restore) (nacv1alpha1.QueueInfo, error) {
var queueInfo nacv1alpha1.QueueInfo

// If the target restore has no valid CreationTimestamp, it means that it's not yet reconciled by OADP/Velero.
// In this case, we can't determine its queue position, so we return nil.
if targetRestore == nil || targetRestore.CreationTimestamp.IsZero() {
return queueInfo, nil
}

// If the target restore has a CompletionTimestamp, it means that it's already served.
if targetRestore.Status.CompletionTimestamp != nil {
queueInfo.EstimatedQueuePosition = 0
return queueInfo, nil
}

// List all Restore objects in the namespace
var restoreList velerov1.RestoreList
if err := clientInstance.List(ctx, &restoreList, client.InNamespace(namespace)); err != nil {
return queueInfo, err
}

// Extract the target restore's creation timestamp
targetTimestamp := targetRestore.CreationTimestamp.Time

// The target restore is always in queue at least in the first position
// 0 is reserved for the restores that are already served.
queueInfo.EstimatedQueuePosition = 1

// Iterate through restores and calculate position
for i := range restoreList.Items {
restore := &restoreList.Items[i]

// Skip restores that have CompletionTimestamp set. This means that the Velero won't be further processing this restore.
if restore.Status.CompletionTimestamp != nil {
continue
}

// Count restores created earlier than the target restore
if restore.CreationTimestamp.Time.Before(targetTimestamp) {
queueInfo.EstimatedQueuePosition++
}
}

return queueInfo, nil
}

// GetVeleroDeleteBackupRequestByLabel retrieves a DeleteBackupRequest object based on a specified label within a given namespace.
// It returns the DeleteBackupRequest only when exactly one object is found, throws an error if multiple backups are found,
// or returns nil if no matches are found.
Expand Down Expand Up @@ -350,7 +423,7 @@ func GetVeleroRestoreByLabel(ctx context.Context, clientInstance client.Client,
case 1:
return &veleroRestoreList.Items[0], nil
default:
return nil, fmt.Errorf("multiple Velero Restore objects found with label %s=%s in namespace '%s'", constant.NarOriginNACUUIDLabel, labelValue, namespace)
return nil, fmt.Errorf("multiple VeleroRestore objects found with label %s=%s in namespace '%s'", constant.NarOriginNACUUIDLabel, labelValue, namespace)
}
}

Expand Down
211 changes: 205 additions & 6 deletions internal/common/function/function_test.go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is no test for GetRestoreQueueInfo function, right? would be interesting to add it?

Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ const (
testNonAdminBackupName = "non-admin-backup-name"
testNonAdminSecondBackupName = "non-admin-second-backup-name"
testNonAdminBackupUUID = "12345678-1234-1234-1234-123456789abc"
testNonAdminRestoreName = "non-admin-restore-name"
testNonAdminRestoreUUID = "12345678-1234-1234-1234-123456789abc"
testNonAdminRestoreNamespace = "non-admin-restore-namespace"
defaultStr = "default"
invalidInputEmptyNamespace = "Invalid input - empty namespace"
invalidInputEmptyNsErr = "invalid input: namespace, labelKey, and labelValue must not be empty"
expectedIntZero = 0
expectedIntOne = 1
expectedIntTwo = 2
Expand Down Expand Up @@ -826,12 +831,12 @@ func TestGetVeleroBackupByLabel(t *testing.T) {
expectedError: errors.New("multiple VeleroBackup objects found with label openshift.io/oadp-nab-origin-nacuuid=test-app in namespace 'default'"),
},
{
name: "Invalid input - empty namespace",
namespace: "",
name: invalidInputEmptyNamespace,
namespace: constant.EmptyString,
labelValue: testAppStr,
mockBackups: []velerov1.Backup{},
expected: nil,
expectedError: errors.New("invalid input: namespace, labelKey, and labelValue must not be empty"),
expectedError: errors.New(invalidInputEmptyNsErr),
},
}

Expand Down Expand Up @@ -862,6 +867,109 @@ func TestGetVeleroBackupByLabel(t *testing.T) {
}
}

func TestGetVeleroRestoreByLabel(t *testing.T) {
log := zap.New(zap.UseDevMode(true))
ctx := context.Background()
ctx = ctrl.LoggerInto(ctx, log)
scheme := runtime.NewScheme()
const testAppStr = "test-app"

if err := velerov1.AddToScheme(scheme); err != nil {
t.Fatalf("Failed to register VeleroRestore type in TestGetVeleroRestoreByLabel: %v", err)
}

tests := []struct {
name string
namespace string
labelValue string
expected *velerov1.Restore
expectedError error
mockRestores []velerov1.Restore
}{
{
name: "Single VeleroBackup found",
namespace: defaultStr,
labelValue: testAppStr,
mockRestores: []velerov1.Restore{
{
ObjectMeta: metav1.ObjectMeta{
Namespace: defaultStr,
Name: "restore1",
Labels: map[string]string{constant.NarOriginNACUUIDLabel: testAppStr},
},
},
},
expected: &velerov1.Restore{ObjectMeta: metav1.ObjectMeta{Namespace: defaultStr, Name: "restore1", Labels: map[string]string{constant.NarOriginNACUUIDLabel: testAppStr}}},
expectedError: nil,
},
{
name: "No VeleroRestores found",
namespace: defaultStr,
labelValue: testAppStr,
mockRestores: []velerov1.Restore{},
expected: nil,
expectedError: nil,
},
{
name: "Multiple VeleroRestores found",
namespace: defaultStr,
labelValue: testAppStr,
mockRestores: []velerov1.Restore{
{
ObjectMeta: metav1.ObjectMeta{
Namespace: defaultStr,
Name: "restore2",
Labels: map[string]string{constant.NarOriginNACUUIDLabel: testAppStr},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Namespace: defaultStr,
Name: "restore3",
Labels: map[string]string{constant.NarOriginNACUUIDLabel: testAppStr},
},
},
},
expected: nil,
expectedError: errors.New("multiple VeleroRestore objects found with label openshift.io/oadp-nar-origin-nacuuid=test-app in namespace 'default'"),
},
{
name: invalidInputEmptyNamespace,
namespace: constant.EmptyString,
labelValue: testAppStr,
mockRestores: []velerov1.Restore{},
expected: nil,
expectedError: errors.New(invalidInputEmptyNsErr),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var objects []client.Object
for _, restore := range tt.mockRestores {
restoreCopy := restore // Create a copy to avoid memory aliasing
objects = append(objects, &restoreCopy)
}
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build()

result, err := GetVeleroRestoreByLabel(ctx, client, tt.namespace, tt.labelValue)

if tt.expectedError != nil {
assert.EqualError(t, err, tt.expectedError.Error())
} else {
assert.NoError(t, err)
if tt.expected != nil && result != nil {
assert.Equal(t, tt.expected.Name, result.Name, "VeleroRestore Name should match")
assert.Equal(t, tt.expected.Namespace, result.Namespace, "VeleroRestore Namespace should match")
assert.Equal(t, tt.expected.Labels, result.Labels, "VeleroRestore Labels should match")
} else {
assert.Nil(t, result, "Expected result should be nil")
}
}
})
}
}

func TestCheckVeleroBackupMetadata(t *testing.T) {
tests := []struct {
backup *velerov1.Backup
Expand Down Expand Up @@ -1003,6 +1111,97 @@ func TestCheckVeleroBackupMetadata(t *testing.T) {
}
}

func TestCheckVeleroRestoreMetadata(t *testing.T) {
tests := []struct {
restore *velerov1.Restore
name string
expected bool
}{
{
name: "Velero Restore without required non admin labels and annotations",
restore: &velerov1.Restore{},
expected: false,
},
{
name: "Velero Restore without required non admin annotations",
restore: &velerov1.Restore{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
constant.OadpLabel: constant.OadpLabelValue,
constant.ManagedByLabel: constant.ManagedByLabelValue,
},
},
},
expected: false,
},
{
name: "Velero Restore with wrong required non admin label",
restore: &velerov1.Restore{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
constant.OadpLabel: constant.OadpLabelValue,
constant.ManagedByLabel: "foo",
},
},
},
expected: false,
},
{
name: "Velero Restore without required non admin labels",
restore: &velerov1.Restore{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
constant.NarOriginNamespaceAnnotation: testNonAdminRestoreNamespace,
constant.NarOriginNameAnnotation: testNonAdminRestoreName,
},
},
},
expected: false,
},
{
name: "Velero Restore with wrong required non admin annotation [empty]",
restore: &velerov1.Restore{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
constant.OadpLabel: constant.OadpLabelValue,
constant.ManagedByLabel: constant.ManagedByLabelValue,
constant.NarOriginNACUUIDLabel: testNonAdminRestoreUUID,
},
Annotations: map[string]string{
constant.NarOriginNamespaceAnnotation: constant.EmptyString,
constant.NarOriginNameAnnotation: testNonAdminRestoreName,
},
},
},
expected: false,
},
{
name: "Velero Restore with required non admin labels and annotations",
restore: &velerov1.Restore{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
constant.OadpLabel: constant.OadpLabelValue,
constant.ManagedByLabel: constant.ManagedByLabelValue,
constant.NarOriginNACUUIDLabel: testNonAdminRestoreUUID,
},
Annotations: map[string]string{
constant.NarOriginNamespaceAnnotation: testNonAdminRestoreNamespace,
constant.NarOriginNameAnnotation: testNonAdminRestoreName,
},
},
},
expected: true,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := CheckVeleroRestoreMetadata(test.restore)
assert.Equal(t, test.expected, result)
})
}
}

func TestGetVeleroDeleteBackupRequestByLabel(t *testing.T) {
log := zap.New(zap.UseDevMode(true))
ctx := context.Background()
Expand Down Expand Up @@ -1076,12 +1275,12 @@ func TestGetVeleroDeleteBackupRequestByLabel(t *testing.T) {
expectedError: nil,
},
{
name: "Invalid input - empty namespace",
namespace: "",
name: invalidInputEmptyNamespace,
namespace: constant.EmptyString,
labelValue: testAppStr,
mockRequests: []velerov1.DeleteBackupRequest{},
expected: nil,
expectedError: errors.New("invalid input: namespace, labelKey, and labelValue must not be empty"),
expectedError: errors.New(invalidInputEmptyNsErr),
},
}

Expand Down
Loading
Loading