Skip to content

feat(RFE-9399): Add DR CLI lifecycle commands, backup verification, and Agent namespace auto-detection#8685

Open
dpateriya wants to merge 1 commit into
openshift:mainfrom
dpateriya:RFE-9399/dr-cli-lifecycle-commands
Open

feat(RFE-9399): Add DR CLI lifecycle commands, backup verification, and Agent namespace auto-detection#8685
dpateriya wants to merge 1 commit into
openshift:mainfrom
dpateriya:RFE-9399/dr-cli-lifecycle-commands

Conversation

@dpateriya
Copy link
Copy Markdown
Contributor

@dpateriya dpateriya commented Jun 5, 2026

Summary

Implements RFE-9399: adds lifecycle management commands, backup integrity verification, and Agent namespace auto-detection to the HCP DR CLI.

New Commands

Command Description
hypershift get oadp-backups List backups with table/json/yaml output, filterable by HC
hypershift get oadp-restores List restores with table/json/yaml output
hypershift get oadp-schedules List schedules with table/json/yaml output
hypershift destroy oadp-backup --name <name> Delete a backup
hypershift destroy oadp-restore --name <name> Delete a restore
hypershift destroy oadp-schedule --name <name> Delete a schedule
hypershift verify oadp-backup --name <name> Run integrity checks on a backup

New Flag

  • hypershift create oadp-restore --verify — runs backup integrity verification before creating the restore; blocks on failure

Agent Namespace Auto-Detection

  • Automatically reads spec.platform.agent.agentNamespace from the HostedCluster
  • Includes it in backup/restore/schedule namespace list for Agent platform clusters
  • Deduplicates if already specified via --include-additional-namespaces

Verification Checks (verify oadp-backup)

  1. Backup exists
  2. Phase is Completed
  3. Not expired (status.expiration)
  4. Items backed up > 0
  5. Backup Storage Location is Available
  6. Reports warning/error counts

Test Plan

  • Unit tests for VerifyBackup (pass, failed phase, expired)
  • Unit tests for autoIncludeAgentNamespace (AGENT with ns, AGENT without ns, AWS, dedup)
  • Unit tests for table output formatting
  • Unit tests for destroy (exists, not found)
  • go vet passes on all modified packages
  • All existing tests pass (go test ./cmd/oadp/... ./support/oadp/...)
  • Manual testing against live cluster (pending)

Made with Cursor

Summary by CodeRabbit

Release Notes

  • New Features
    • Added hypershift get command to list OADP backups, restores, and schedules with filtering by hosted cluster and multiple output formats (table, JSON, YAML)
    • Added hypershift destroy command to delete OADP resources
    • Added hypershift verify command and --verify flag to validate backup integrity before restore operations
    • Enhanced agent platform support with automatic namespace configuration

…nd Agent namespace auto-detection

Add get/destroy lifecycle commands, backup integrity verification, and
Agent platform namespace auto-detection to the HyperShift DR CLI.

Get commands (hypershift get oadp-backups/restores/schedules):
- List Velero resources with table/json/yaml output
- Filter by --hc-name and --hc-namespace
- Sort by creation timestamp (newest first)

Destroy commands (hypershift destroy oadp-backup/restore/schedule):
- Delete specified Velero resource by name
- Validates existence before deletion

Backup verification (hypershift verify oadp-backup):
- Checks phase, expiration, items count, BSL availability
- Also available as --verify flag on oadp-restore to block
  restore creation when backup integrity checks fail

Agent namespace auto-detection:
- Reads spec.platform.agent.agentNamespace from HostedCluster
- Automatically includes it in backup/restore/schedule namespaces
- Deduplicates if already specified via --include-additional-namespaces

Signed-off-by: Divyam pateriya <dpateriy@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@openshift-merge-bot
Copy link
Copy Markdown
Contributor

Pipeline controller notification
This repo is configured to use the pipeline controller. Second-stage tests will be triggered either automatically or after lgtm label is added, depending on the repository configuration. The pipeline controller will automatically detect which contexts are required and will utilize /test Prow commands to trigger the second stage.

For optional jobs, comment /test ? to see a list of all defined jobs. To trigger manually all jobs from second stage use /pipeline required command.

This repository is configured in: LGTM mode

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 5, 2026

📝 Walkthrough

Walkthrough

This PR adds new get, verify, and destroy subcommands for managing OADP/Velero resources (Backups, Restores, Schedules) in HyperShift. The changes enrich platform metadata capture to extract agent namespaces for agent-based clusters, automatically include agent namespaces during backup configuration, add backup integrity verification before restore creation, enable listing of OADP resources with filtering and multiple output formats, and support deletion of OADP resources. The new commands are registered into the HyperShift CLI hierarchy.

🚥 Pre-merge checks | ✅ 11
✅ Passed checks (11 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the three main features added: DR CLI lifecycle commands (get/destroy), backup verification, and Agent namespace auto-detection, directly matching the changeset scope.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Stable And Deterministic Test Names ✅ Passed PR introduces standard Go tests (not Ginkgo) with stable, descriptive, deterministic test case names free of dynamic values like timestamps, UUIDs, or generated identifiers.
Test Structure And Quality ✅ Passed Check is for "Ginkgo test code" but PR only has standard Go testing.T tests in lifecycle_test.go; no Ginkgo tests present in PR.
Topology-Aware Scheduling Compatibility ✅ Passed PR adds only CLI commands and validation helpers; no Kubernetes resource definitions, manifests, or scheduling constraints are introduced or modified.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed No Ginkgo e2e tests were added in this PR. Only CLI commands and a unit test (using standard testing package) were added.
No-Weak-Crypto ✅ Passed No weak crypto primitives found. PR uses only SHA-256 in cmd/oadp/common.go for deterministic resource naming—appropriate for non-security hashing.
Container-Privileges ✅ Passed PR contains only Go CLI command code with no container/Kubernetes manifests or privileged container configurations.
No-Sensitive-Data-In-Logs ✅ Passed No sensitive data (passwords, tokens, credentials, PII, etc.) is logged in the new code. All logging contains only non-sensitive metadata like status values, object names, and operation summaries.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@openshift-ci
Copy link
Copy Markdown
Contributor

openshift-ci Bot commented Jun 5, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: dpateriya
Once this PR has been reviewed and has the lgtm label, please assign jparrill for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci openshift-ci Bot requested review from sdminonne and sjenning June 5, 2026 14:48
@openshift-ci openshift-ci Bot added area/cli Indicates the PR includes changes for CLI area/control-plane-operator Indicates the PR includes changes for the control plane operator - in an OCP release area/hypershift-operator Indicates the PR includes changes for the hypershift operator and API - outside an OCP release and removed do-not-merge/needs-area labels Jun 5, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 5, 2026

Codecov Report

❌ Patch coverage is 23.71134% with 370 lines in your changes missing coverage. Please review.
✅ Project coverage is 41.33%. Comparing base (f13c62d) to head (bbe67e9).

Files with missing lines Patch % Lines
cmd/oadp/get.go 11.34% 171 Missing and 1 partial ⚠️
cmd/oadp/verify_backup.go 39.43% 80 Missing and 6 partials ⚠️
cmd/oadp/destroy.go 14.86% 61 Missing and 2 partials ⚠️
cmd/oadp/restore.go 0.00% 19 Missing ⚠️
cmd/get/get.go 0.00% 12 Missing ⚠️
cmd/verify/verify.go 0.00% 10 Missing ⚠️
cmd/destroy/destroy.go 0.00% 3 Missing ⚠️
cmd/oadp/backup.go 0.00% 3 Missing ⚠️
support/oadp/validate.go 87.50% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8685      +/-   ##
==========================================
- Coverage   41.43%   41.33%   -0.10%     
==========================================
  Files         756      761       +5     
  Lines       93647    94124     +477     
==========================================
+ Hits        38802    38910     +108     
- Misses      52124    52483     +359     
- Partials     2721     2731      +10     
Files with missing lines Coverage Δ
cmd/oadp/common.go 90.16% <100.00%> (+0.50%) ⬆️
cmd/oadp/schedule.go 68.27% <100.00%> (+0.08%) ⬆️
support/oadp/validate.go 86.17% <87.50%> (-1.21%) ⬇️
cmd/destroy/destroy.go 0.00% <0.00%> (ø)
cmd/oadp/backup.go 45.22% <0.00%> (-0.23%) ⬇️
cmd/verify/verify.go 0.00% <0.00%> (ø)
cmd/get/get.go 0.00% <0.00%> (ø)
cmd/oadp/restore.go 34.24% <0.00%> (-2.10%) ⬇️
cmd/oadp/destroy.go 14.86% <14.86%> (ø)
cmd/oadp/verify_backup.go 39.43% <39.43%> (ø)
... and 1 more
Flag Coverage Δ
cmd-support 34.69% <23.71%> (-0.19%) ⬇️
cpo-hostedcontrolplane 43.50% <ø> (ø)
cpo-other 42.74% <ø> (ø)
hypershift-operator 51.57% <ø> (ø)
other 31.64% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
cmd/oadp/verify_backup.go (1)

106-106: 💤 Low value

Unused logger parameter.

The logr.Logger parameter is unused (named _). Consider removing it or using it for internal diagnostic logging within the verification checks.

♻️ Optional: Remove unused parameter
-func VerifyBackup(ctx context.Context, c client.Client, backupName, oadpNamespace string, _ logr.Logger) ([]VerifyResult, error) {
+func VerifyBackup(ctx context.Context, c client.Client, backupName, oadpNamespace string) ([]VerifyResult, error) {

Then update the callers in verify_backup.go line 81 and restore.go line 205 to remove the logger argument.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/oadp/verify_backup.go` at line 106, The VerifyBackup function currently
declares an unused logr.Logger parameter (named `_`). Fix this by either
removing the logger parameter from VerifyBackup's signature and deleting the
logger argument at all call sites that pass it, or by renaming `_` to `logger`
and using it for diagnostic logs inside VerifyBackup (e.g., logging
start/step/results); ensure you update every invocation of VerifyBackup to match
the new signature so callers no longer pass an unused logger or instead pass and
the function uses it.
cmd/oadp/lifecycle_test.go (4)

141-141: 💤 Low value

Remove or implement verification for expectLogMessage field.

The expectLogMessage field is declared and set in test cases but never used in the test logic. Either remove it or implement log output verification if you intend to test logging behavior.

From common.go:283 (context snippet 2), autoIncludeAgentNamespace logs: "Auto-detected agent namespace, including in operation". If log verification is desired, consider using a test logger sink to capture and verify log output.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/oadp/lifecycle_test.go` at line 141, The test struct field
expectLogMessage is declared and unused; either remove it from the test cases in
lifecycle_test.go or implement log verification: when testing behavior that
triggers autoIncludeAgentNamespace (which logs "Auto-detected agent namespace,
including in operation"), wire a test logger sink or capture output in the test,
run the code path that calls autoIncludeAgentNamespace, and assert the captured
logs contain the expected message; update or remove references to
expectLogMessage accordingly so the field is not left unused.

283-286: ⚡ Quick win

Verify that the backup was actually deleted.

The test only checks that runDestroy returns no error but doesn't verify the side effect—that the backup was actually removed from the fake client. Add a verification step:

✅ Suggested verification
 	err := opts.runDestroy(ctx, "Backup")
 	if err != nil {
 		t.Fatalf("expected no error, got: %v", err)
 	}
+
+	// Verify the backup was actually deleted
+	deletedBackup := &unstructured.Unstructured{}
+	deletedBackup.SetAPIVersion("velero.io/v1")
+	deletedBackup.SetKind("Backup")
+	key := client.ObjectKey{Name: "to-delete", Namespace: "openshift-adp"}
+	err = fakeClient.Get(ctx, key, deletedBackup)
+	if err == nil {
+		t.Error("expected backup to be deleted, but it still exists")
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/oadp/lifecycle_test.go` around lines 283 - 286, After calling
opts.runDestroy(ctx, "Backup"), add an assertion that the fake backup client no
longer contains the Backup resource: query the same fake client used in the test
(e.g., the test's fake backup client referenced by opts or the test setup) with
a Get/List for the "Backup" name/label and fail the test if the resource is
still present (expect NotFound or zero results); this ensures runDestroy not
only returns nil but actually removed the backup.

196-199: 💤 Low value

Consider order-independent slice comparison.

The current index-based comparison (lines 196-199) assumes both slices have the same order. While this works for the current implementation, a set-based comparison would be more robust if the append order ever changes:

♻️ Optional: More robust comparison
-			for i, ns := range tt.expectedNS {
-				if i >= len(opts.IncludeNamespaces) || opts.IncludeNamespaces[i] != ns {
-					t.Errorf("expected namespace[%d]=%q, got %v", i, ns, opts.IncludeNamespaces)
-				}
-			}
+			expectedSet := make(map[string]bool)
+			for _, ns := range tt.expectedNS {
+				expectedSet[ns] = true
+			}
+			for _, ns := range opts.IncludeNamespaces {
+				if !expectedSet[ns] {
+					t.Errorf("unexpected namespace %q in result", ns)
+				}
+				delete(expectedSet, ns)
+			}
+			for ns := range expectedSet {
+				t.Errorf("expected namespace %q not found in result", ns)
+			}

Or with gomega (per earlier suggestion): g.Expect(opts.IncludeNamespaces).To(ConsistOf(tt.expectedNS)).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/oadp/lifecycle_test.go` around lines 196 - 199, The test currently
compares opts.IncludeNamespaces to tt.expectedNS with an index-based loop which
assumes identical order; change this to an order-independent comparison by first
verifying lengths match and then either (a) sort both slices and compare
equality, or (b) build a set/map from one slice and assert every element of the
other is present (and no extras). Locate the check using opts.IncludeNamespaces
and tt.expectedNS in the test and replace the index loop with one of these
set-based or sorted-comparison approaches (alternatively, if using Gomega,
replace the assertion with
g.Expect(opts.IncludeNamespaces).To(ConsistOf(tt.expectedNS))).

25-48: ⚡ Quick win

Add test coverage for BackupStorageLocation checks.

The test case at Line 36 sets spec.storageLocation: "default", but no BackupStorageLocation object is added to the fake client. According to verify_backup.go:159-175 (context snippet 1), VerifyBackup attempts to fetch and validate the BSL's phase. Without a BSL object in the fake client, this check isn't exercised.

Add a test case that includes a BSL object to verify the storage_location check works correctly:

🧪 Suggested test case with BSL
{
	name: "When backup references available BSL it should pass storage location check",
	backup: map[string]interface{}{
		"apiVersion": "velero.io/v1",
		"kind":       "Backup",
		"metadata": map[string]interface{}{
			"name":              "test-backup-bsl",
			"namespace":         "openshift-adp",
			"creationTimestamp": "2026-06-01T10:00:00Z",
		},
		"spec": map[string]interface{}{
			"storageLocation": "default",
		},
		"status": map[string]interface{}{
			"phase": "Completed",
			"progress": map[string]interface{}{
				"itemsBackedUp": int64(10),
			},
		},
	},
	bsl: &unstructured.Unstructured{
		Object: map[string]interface{}{
			"apiVersion": "velero.io/v1",
			"kind":       "BackupStorageLocation",
			"metadata": map[string]interface{}{
				"name":      "default",
				"namespace": "openshift-adp",
			},
			"status": map[string]interface{}{
				"phase": "Available",
			},
		},
	},
	expectError: false,
},

Update the test loop to add the BSL to the fake client when tt.bsl != nil.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/oadp/lifecycle_test.go` around lines 25 - 48, The test currently sets
spec.storageLocation but never adds a BackupStorageLocation object so
VerifyBackup's storage_location check isn't exercised; add a new subtest (e.g.,
name "When backup references available BSL it should pass storage location
check") that includes tt.bsl as an *unstructured.Unstructured
BackupStorageLocation with metadata.name "default", metadata.namespace
"openshift-adp" and status.phase "Available", and update the test setup loop to
inject tt.bsl into the fake client when tt.bsl != nil so VerifyBackup (the
function performing the BSL fetch/validation) exercises the BSL phase check for
storageLocation "default".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/oadp/lifecycle_test.go`:
- Around line 18-308: Tests use raw testing assertions
(t.Errorf/t.Fatalf/t.Error/t.Fatal); update all five tests (TestVerifyBackup,
TestAutoIncludeAgentNamespace, TestGetOutputTable, TestDestroyOptions,
TestDestroyOptionsNotFound) to use Gomega matchers. For each test create g :=
NewWithT(t) and replace error checks with g.Expect(err).To(HaveOccurred()) or
g.Expect(err).NotTo(HaveOccurred()), replace equality/length/contain checks with
g.Expect(...).To(Equal(...)), g.Expect(...).To(HaveLen(...)),
g.Expect(stringOutput).To(ContainSubstring("...")), and assert boolean
conditions with g.Expect(...).To(BeTrue()/BeFalse()); also replace manual
map/slice existence checks in TestVerifyBackup/TestAutoIncludeAgentNamespace
with appropriate Expect assertions. Add the gomega import (NewWithT, matchers)
and remove t.* assertion calls.

In `@cmd/oadp/restore.go`:
- Around line 202-222: Validate and handle the case where verification was
requested but no explicit backup name was provided: inside the RunE entry point,
add a check for o.VerifyBackup combined with a schedule-based restore (i.e.,
when o.BackupName is empty or a boolean/field representing --from-schedule is
set) and either return a user-facing error (making --verify and --from-schedule
mutually exclusive) or emit a clear warning before proceeding; update the logic
that currently checks o.VerifyBackup && o.BackupName != "" in the restore flow
(the block using VerifyBackup(ctx, o.Client, o.BackupName, o.OADPNamespace,
o.Log)) so it cannot be silently skipped when the user requested verification
(reference o.VerifyBackup, o.BackupName, RunE and the VerifyBackup call).

---

Nitpick comments:
In `@cmd/oadp/lifecycle_test.go`:
- Line 141: The test struct field expectLogMessage is declared and unused;
either remove it from the test cases in lifecycle_test.go or implement log
verification: when testing behavior that triggers autoIncludeAgentNamespace
(which logs "Auto-detected agent namespace, including in operation"), wire a
test logger sink or capture output in the test, run the code path that calls
autoIncludeAgentNamespace, and assert the captured logs contain the expected
message; update or remove references to expectLogMessage accordingly so the
field is not left unused.
- Around line 283-286: After calling opts.runDestroy(ctx, "Backup"), add an
assertion that the fake backup client no longer contains the Backup resource:
query the same fake client used in the test (e.g., the test's fake backup client
referenced by opts or the test setup) with a Get/List for the "Backup"
name/label and fail the test if the resource is still present (expect NotFound
or zero results); this ensures runDestroy not only returns nil but actually
removed the backup.
- Around line 196-199: The test currently compares opts.IncludeNamespaces to
tt.expectedNS with an index-based loop which assumes identical order; change
this to an order-independent comparison by first verifying lengths match and
then either (a) sort both slices and compare equality, or (b) build a set/map
from one slice and assert every element of the other is present (and no extras).
Locate the check using opts.IncludeNamespaces and tt.expectedNS in the test and
replace the index loop with one of these set-based or sorted-comparison
approaches (alternatively, if using Gomega, replace the assertion with
g.Expect(opts.IncludeNamespaces).To(ConsistOf(tt.expectedNS))).
- Around line 25-48: The test currently sets spec.storageLocation but never adds
a BackupStorageLocation object so VerifyBackup's storage_location check isn't
exercised; add a new subtest (e.g., name "When backup references available BSL
it should pass storage location check") that includes tt.bsl as an
*unstructured.Unstructured BackupStorageLocation with metadata.name "default",
metadata.namespace "openshift-adp" and status.phase "Available", and update the
test setup loop to inject tt.bsl into the fake client when tt.bsl != nil so
VerifyBackup (the function performing the BSL fetch/validation) exercises the
BSL phase check for storageLocation "default".

In `@cmd/oadp/verify_backup.go`:
- Line 106: The VerifyBackup function currently declares an unused logr.Logger
parameter (named `_`). Fix this by either removing the logger parameter from
VerifyBackup's signature and deleting the logger argument at all call sites that
pass it, or by renaming `_` to `logger` and using it for diagnostic logs inside
VerifyBackup (e.g., logging start/step/results); ensure you update every
invocation of VerifyBackup to match the new signature so callers no longer pass
an unused logger or instead pass and the function uses it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: 9e2360ff-1aa5-4fc8-81f3-6d9704d38cec

📥 Commits

Reviewing files that changed from the base of the PR and between f13c62d and bbe67e9.

📒 Files selected for processing (14)
  • cmd/destroy/destroy.go
  • cmd/get/get.go
  • cmd/oadp/backup.go
  • cmd/oadp/common.go
  • cmd/oadp/destroy.go
  • cmd/oadp/get.go
  • cmd/oadp/lifecycle_test.go
  • cmd/oadp/restore.go
  • cmd/oadp/schedule.go
  • cmd/oadp/types.go
  • cmd/oadp/verify_backup.go
  • cmd/verify/verify.go
  • main.go
  • support/oadp/validate.go

Comment on lines +18 to +308
func TestVerifyBackup(t *testing.T) {
tests := []struct {
name string
backup map[string]interface{}
expectError bool
failChecks []string
}{
{
name: "When backup is completed with valid items it should pass all checks",
backup: map[string]interface{}{
"apiVersion": "velero.io/v1",
"kind": "Backup",
"metadata": map[string]interface{}{
"name": "test-backup",
"namespace": "openshift-adp",
"creationTimestamp": "2026-06-01T10:00:00Z",
},
"spec": map[string]interface{}{
"storageLocation": "default",
},
"status": map[string]interface{}{
"phase": "Completed",
"expiration": time.Now().Add(24 * time.Hour).Format(time.RFC3339),
"progress": map[string]interface{}{
"itemsBackedUp": int64(142),
"totalItems": int64(142),
},
},
},
expectError: false,
},
{
name: "When backup has failed phase it should fail the phase check",
backup: map[string]interface{}{
"apiVersion": "velero.io/v1",
"kind": "Backup",
"metadata": map[string]interface{}{
"name": "failed-backup",
"namespace": "openshift-adp",
"creationTimestamp": "2026-06-01T10:00:00Z",
},
"spec": map[string]interface{}{},
"status": map[string]interface{}{
"phase": "Failed",
"progress": map[string]interface{}{
"itemsBackedUp": int64(0),
"totalItems": int64(100),
},
},
},
expectError: false,
failChecks: []string{"phase", "items"},
},
{
name: "When backup has expired it should fail the expiration check",
backup: map[string]interface{}{
"apiVersion": "velero.io/v1",
"kind": "Backup",
"metadata": map[string]interface{}{
"name": "expired-backup",
"namespace": "openshift-adp",
"creationTimestamp": "2026-06-01T10:00:00Z",
},
"spec": map[string]interface{}{},
"status": map[string]interface{}{
"phase": "Completed",
"expiration": time.Now().Add(-2 * time.Hour).Format(time.RFC3339),
"progress": map[string]interface{}{
"itemsBackedUp": int64(142),
"totalItems": int64(142),
},
},
},
expectError: false,
failChecks: []string{"expiration"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
scheme := runtime.NewScheme()

backupObj := &unstructured.Unstructured{Object: tt.backup}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(backupObj).
Build()

results, err := VerifyBackup(ctx, fakeClient, backupObj.GetName(), "openshift-adp", log.Log)
if tt.expectError {
if err == nil {
t.Errorf("expected error but got none")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

failedChecks := make(map[string]bool)
for _, r := range results {
if !r.Passed {
failedChecks[r.Check] = true
}
}

for _, expected := range tt.failChecks {
if !failedChecks[expected] {
t.Errorf("expected check '%s' to fail, but it passed", expected)
}
}
})
}
}

func TestAutoIncludeAgentNamespace(t *testing.T) {
tests := []struct {
name string
platform string
agentNamespace string
existingNS []string
expectedNS []string
expectLogMessage bool
}{
{
name: "When platform is AGENT with namespace it should auto-include",
platform: "AGENT",
agentNamespace: "my-agent-ns",
existingNS: nil,
expectedNS: []string{"my-agent-ns"},
expectLogMessage: true,
},
{
name: "When platform is AGENT and namespace already included it should not duplicate",
platform: "AGENT",
agentNamespace: "my-agent-ns",
existingNS: []string{"my-agent-ns"},
expectedNS: []string{"my-agent-ns"},
expectLogMessage: false,
},
{
name: "When platform is AWS it should not include agent namespace",
platform: "AWS",
agentNamespace: "",
existingNS: nil,
expectedNS: nil,
expectLogMessage: false,
},
{
name: "When platform is AGENT but namespace is empty it should skip",
platform: "AGENT",
agentNamespace: "",
existingNS: nil,
expectedNS: nil,
expectLogMessage: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
opts := &CreateOptions{
Log: log.Log,
IncludeNamespaces: tt.existingNS,
}
info := &supportoadp.PlatformInfo{
Type: tt.platform,
AgentNamespace: tt.agentNamespace,
}

autoIncludeAgentNamespace(opts, info)

if len(tt.expectedNS) == 0 && len(opts.IncludeNamespaces) == 0 {
return
}
if len(opts.IncludeNamespaces) != len(tt.expectedNS) {
t.Errorf("expected namespaces %v, got %v", tt.expectedNS, opts.IncludeNamespaces)
}
for i, ns := range tt.expectedNS {
if i >= len(opts.IncludeNamespaces) || opts.IncludeNamespaces[i] != ns {
t.Errorf("expected namespace[%d]=%q, got %v", i, ns, opts.IncludeNamespaces)
}
}
})
}
}

func TestGetOutputTable(t *testing.T) {
items := []unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "velero.io/v1",
"kind": "Backup",
"metadata": map[string]interface{}{
"name": "test-backup-1",
"namespace": "openshift-adp",
"creationTimestamp": time.Now().Add(-2 * time.Hour).Format(time.RFC3339),
},
"status": map[string]interface{}{
"phase": "Completed",
},
},
},
{
Object: map[string]interface{}{
"apiVersion": "velero.io/v1",
"kind": "Backup",
"metadata": map[string]interface{}{
"name": "test-backup-2",
"namespace": "openshift-adp",
"creationTimestamp": time.Now().Add(-48 * time.Hour).Format(time.RFC3339),
},
"status": map[string]interface{}{
"phase": "Failed",
},
},
},
}

var buf bytes.Buffer
err := outputTable(&buf, items, "Backup")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

output := buf.String()
if !bytes.Contains([]byte(output), []byte("NAME")) {
t.Error("table output should contain NAME header")
}
if !bytes.Contains([]byte(output), []byte("test-backup-1")) {
t.Error("table output should contain test-backup-1")
}
if !bytes.Contains([]byte(output), []byte("Completed")) {
t.Error("table output should contain Completed status")
}
}

func TestDestroyOptions(t *testing.T) {
ctx := context.Background()
scheme := runtime.NewScheme()

backup := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "velero.io/v1",
"kind": "Backup",
"metadata": map[string]interface{}{
"name": "to-delete",
"namespace": "openshift-adp",
},
"spec": map[string]interface{}{},
},
}

fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(backup).
Build()

opts := &DestroyOptions{
Name: "to-delete",
OADPNamespace: "openshift-adp",
Log: log.Log,
Client: fakeClient,
}

err := opts.runDestroy(ctx, "Backup")
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
}

func TestDestroyOptionsNotFound(t *testing.T) {
ctx := context.Background()
scheme := runtime.NewScheme()

fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
Build()

opts := &DestroyOptions{
Name: "nonexistent",
OADPNamespace: "openshift-adp",
Log: log.Log,
Client: fakeClient,
}

err := opts.runDestroy(ctx, "Backup")
if err == nil {
t.Fatal("expected error for nonexistent backup, got none")
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use gomega for test assertions instead of standard testing conditionals.

This file uses standard testing package assertions (t.Errorf, t.Fatalf, t.Error, t.Fatal) throughout all five test functions. As per coding guidelines, prefer gomega for unit test assertions to get better failure messages and more expressive matchers.

Example conversion for TestVerifyBackup:

+import (
+	. "github.com/onsi/gomega"
+)
+
 func TestVerifyBackup(t *testing.T) {
+	g := NewWithT(t)
 	tests := []struct {
 		...
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			...
 			results, err := VerifyBackup(ctx, fakeClient, backupObj.GetName(), "openshift-adp", log.Log)
 			if tt.expectError {
-				if err == nil {
-					t.Errorf("expected error but got none")
-				}
+				g.Expect(err).To(HaveOccurred(), "expected error but got none")
 				return
 			}
-			if err != nil {
-				t.Fatalf("unexpected error: %v", err)
-			}
+			g.Expect(err).NotTo(HaveOccurred())
 			
 			failedChecks := make(map[string]bool)
 			for _, r := range results {
 				if !r.Passed {
 					failedChecks[r.Check] = true
 				}
 			}
 			
 			for _, expected := range tt.failChecks {
-				if !failedChecks[expected] {
-					t.Errorf("expected check '%s' to fail, but it passed", expected)
-				}
+				g.Expect(failedChecks).To(HaveKey(expected), "expected check '%s' to fail", expected)
 			}
 		})
 	}
 }

Apply similar conversions to the other test functions.

As per coding guidelines: "Prefer gomega for unit test assertions" (applies to **/*_test.go).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestVerifyBackup(t *testing.T) {
tests := []struct {
name string
backup map[string]interface{}
expectError bool
failChecks []string
}{
{
name: "When backup is completed with valid items it should pass all checks",
backup: map[string]interface{}{
"apiVersion": "velero.io/v1",
"kind": "Backup",
"metadata": map[string]interface{}{
"name": "test-backup",
"namespace": "openshift-adp",
"creationTimestamp": "2026-06-01T10:00:00Z",
},
"spec": map[string]interface{}{
"storageLocation": "default",
},
"status": map[string]interface{}{
"phase": "Completed",
"expiration": time.Now().Add(24 * time.Hour).Format(time.RFC3339),
"progress": map[string]interface{}{
"itemsBackedUp": int64(142),
"totalItems": int64(142),
},
},
},
expectError: false,
},
{
name: "When backup has failed phase it should fail the phase check",
backup: map[string]interface{}{
"apiVersion": "velero.io/v1",
"kind": "Backup",
"metadata": map[string]interface{}{
"name": "failed-backup",
"namespace": "openshift-adp",
"creationTimestamp": "2026-06-01T10:00:00Z",
},
"spec": map[string]interface{}{},
"status": map[string]interface{}{
"phase": "Failed",
"progress": map[string]interface{}{
"itemsBackedUp": int64(0),
"totalItems": int64(100),
},
},
},
expectError: false,
failChecks: []string{"phase", "items"},
},
{
name: "When backup has expired it should fail the expiration check",
backup: map[string]interface{}{
"apiVersion": "velero.io/v1",
"kind": "Backup",
"metadata": map[string]interface{}{
"name": "expired-backup",
"namespace": "openshift-adp",
"creationTimestamp": "2026-06-01T10:00:00Z",
},
"spec": map[string]interface{}{},
"status": map[string]interface{}{
"phase": "Completed",
"expiration": time.Now().Add(-2 * time.Hour).Format(time.RFC3339),
"progress": map[string]interface{}{
"itemsBackedUp": int64(142),
"totalItems": int64(142),
},
},
},
expectError: false,
failChecks: []string{"expiration"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
scheme := runtime.NewScheme()
backupObj := &unstructured.Unstructured{Object: tt.backup}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(backupObj).
Build()
results, err := VerifyBackup(ctx, fakeClient, backupObj.GetName(), "openshift-adp", log.Log)
if tt.expectError {
if err == nil {
t.Errorf("expected error but got none")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
failedChecks := make(map[string]bool)
for _, r := range results {
if !r.Passed {
failedChecks[r.Check] = true
}
}
for _, expected := range tt.failChecks {
if !failedChecks[expected] {
t.Errorf("expected check '%s' to fail, but it passed", expected)
}
}
})
}
}
func TestAutoIncludeAgentNamespace(t *testing.T) {
tests := []struct {
name string
platform string
agentNamespace string
existingNS []string
expectedNS []string
expectLogMessage bool
}{
{
name: "When platform is AGENT with namespace it should auto-include",
platform: "AGENT",
agentNamespace: "my-agent-ns",
existingNS: nil,
expectedNS: []string{"my-agent-ns"},
expectLogMessage: true,
},
{
name: "When platform is AGENT and namespace already included it should not duplicate",
platform: "AGENT",
agentNamespace: "my-agent-ns",
existingNS: []string{"my-agent-ns"},
expectedNS: []string{"my-agent-ns"},
expectLogMessage: false,
},
{
name: "When platform is AWS it should not include agent namespace",
platform: "AWS",
agentNamespace: "",
existingNS: nil,
expectedNS: nil,
expectLogMessage: false,
},
{
name: "When platform is AGENT but namespace is empty it should skip",
platform: "AGENT",
agentNamespace: "",
existingNS: nil,
expectedNS: nil,
expectLogMessage: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
opts := &CreateOptions{
Log: log.Log,
IncludeNamespaces: tt.existingNS,
}
info := &supportoadp.PlatformInfo{
Type: tt.platform,
AgentNamespace: tt.agentNamespace,
}
autoIncludeAgentNamespace(opts, info)
if len(tt.expectedNS) == 0 && len(opts.IncludeNamespaces) == 0 {
return
}
if len(opts.IncludeNamespaces) != len(tt.expectedNS) {
t.Errorf("expected namespaces %v, got %v", tt.expectedNS, opts.IncludeNamespaces)
}
for i, ns := range tt.expectedNS {
if i >= len(opts.IncludeNamespaces) || opts.IncludeNamespaces[i] != ns {
t.Errorf("expected namespace[%d]=%q, got %v", i, ns, opts.IncludeNamespaces)
}
}
})
}
}
func TestGetOutputTable(t *testing.T) {
items := []unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "velero.io/v1",
"kind": "Backup",
"metadata": map[string]interface{}{
"name": "test-backup-1",
"namespace": "openshift-adp",
"creationTimestamp": time.Now().Add(-2 * time.Hour).Format(time.RFC3339),
},
"status": map[string]interface{}{
"phase": "Completed",
},
},
},
{
Object: map[string]interface{}{
"apiVersion": "velero.io/v1",
"kind": "Backup",
"metadata": map[string]interface{}{
"name": "test-backup-2",
"namespace": "openshift-adp",
"creationTimestamp": time.Now().Add(-48 * time.Hour).Format(time.RFC3339),
},
"status": map[string]interface{}{
"phase": "Failed",
},
},
},
}
var buf bytes.Buffer
err := outputTable(&buf, items, "Backup")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
output := buf.String()
if !bytes.Contains([]byte(output), []byte("NAME")) {
t.Error("table output should contain NAME header")
}
if !bytes.Contains([]byte(output), []byte("test-backup-1")) {
t.Error("table output should contain test-backup-1")
}
if !bytes.Contains([]byte(output), []byte("Completed")) {
t.Error("table output should contain Completed status")
}
}
func TestDestroyOptions(t *testing.T) {
ctx := context.Background()
scheme := runtime.NewScheme()
backup := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "velero.io/v1",
"kind": "Backup",
"metadata": map[string]interface{}{
"name": "to-delete",
"namespace": "openshift-adp",
},
"spec": map[string]interface{}{},
},
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(backup).
Build()
opts := &DestroyOptions{
Name: "to-delete",
OADPNamespace: "openshift-adp",
Log: log.Log,
Client: fakeClient,
}
err := opts.runDestroy(ctx, "Backup")
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
}
func TestDestroyOptionsNotFound(t *testing.T) {
ctx := context.Background()
scheme := runtime.NewScheme()
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
Build()
opts := &DestroyOptions{
Name: "nonexistent",
OADPNamespace: "openshift-adp",
Log: log.Log,
Client: fakeClient,
}
err := opts.runDestroy(ctx, "Backup")
if err == nil {
t.Fatal("expected error for nonexistent backup, got none")
}
}
import (
. "github.com/onsi/gomega"
)
func TestVerifyBackup(t *testing.T) {
g := NewWithT(t)
tests := []struct {
name string
backup map[string]interface{}
expectError bool
failChecks []string
}{
{
name: "When backup is completed with valid items it should pass all checks",
backup: map[string]interface{}{
"apiVersion": "velero.io/v1",
"kind": "Backup",
"metadata": map[string]interface{}{
"name": "test-backup",
"namespace": "openshift-adp",
"creationTimestamp": "2026-06-01T10:00:00Z",
},
"spec": map[string]interface{}{
"storageLocation": "default",
},
"status": map[string]interface{}{
"phase": "Completed",
"expiration": time.Now().Add(24 * time.Hour).Format(time.RFC3339),
"progress": map[string]interface{}{
"itemsBackedUp": int64(142),
"totalItems": int64(142),
},
},
},
expectError: false,
},
{
name: "When backup has failed phase it should fail the phase check",
backup: map[string]interface{}{
"apiVersion": "velero.io/v1",
"kind": "Backup",
"metadata": map[string]interface{}{
"name": "failed-backup",
"namespace": "openshift-adp",
"creationTimestamp": "2026-06-01T10:00:00Z",
},
"spec": map[string]interface{}{},
"status": map[string]interface{}{
"phase": "Failed",
"progress": map[string]interface{}{
"itemsBackedUp": int64(0),
"totalItems": int64(100),
},
},
},
expectError: false,
failChecks: []string{"phase", "items"},
},
{
name: "When backup has expired it should fail the expiration check",
backup: map[string]interface{}{
"apiVersion": "velero.io/v1",
"kind": "Backup",
"metadata": map[string]interface{}{
"name": "expired-backup",
"namespace": "openshift-adp",
"creationTimestamp": "2026-06-01T10:00:00Z",
},
"spec": map[string]interface{}{},
"status": map[string]interface{}{
"phase": "Completed",
"expiration": time.Now().Add(-2 * time.Hour).Format(time.RFC3339),
"progress": map[string]interface{}{
"itemsBackedUp": int64(142),
"totalItems": int64(142),
},
},
},
expectError: false,
failChecks: []string{"expiration"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
scheme := runtime.NewScheme()
backupObj := &unstructured.Unstructured{Object: tt.backup}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(backupObj).
Build()
results, err := VerifyBackup(ctx, fakeClient, backupObj.GetName(), "openshift-adp", log.Log)
g.Expect(err).To(HaveOccurred(), "expected error but got none")
if tt.expectError {
return
}
g.Expect(err).NotTo(HaveOccurred())
failedChecks := make(map[string]bool)
for _, r := range results {
if !r.Passed {
failedChecks[r.Check] = true
}
}
for _, expected := range tt.failChecks {
g.Expect(failedChecks).To(HaveKey(expected), "expected check '%s' to fail", expected)
}
})
}
}
func TestAutoIncludeAgentNamespace(t *testing.T) {
tests := []struct {
name string
platform string
agentNamespace string
existingNS []string
expectedNS []string
expectLogMessage bool
}{
{
name: "When platform is AGENT with namespace it should auto-include",
platform: "AGENT",
agentNamespace: "my-agent-ns",
existingNS: nil,
expectedNS: []string{"my-agent-ns"},
expectLogMessage: true,
},
{
name: "When platform is AGENT and namespace already included it should not duplicate",
platform: "AGENT",
agentNamespace: "my-agent-ns",
existingNS: []string{"my-agent-ns"},
expectedNS: []string{"my-agent-ns"},
expectLogMessage: false,
},
{
name: "When platform is AWS it should not include agent namespace",
platform: "AWS",
agentNamespace: "",
existingNS: nil,
expectedNS: nil,
expectLogMessage: false,
},
{
name: "When platform is AGENT but namespace is empty it should skip",
platform: "AGENT",
agentNamespace: "",
existingNS: nil,
expectedNS: nil,
expectLogMessage: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
opts := &CreateOptions{
Log: log.Log,
IncludeNamespaces: tt.existingNS,
}
info := &supportoadp.PlatformInfo{
Type: tt.platform,
AgentNamespace: tt.agentNamespace,
}
autoIncludeAgentNamespace(opts, info)
if len(tt.expectedNS) == 0 && len(opts.IncludeNamespaces) == 0 {
return
}
if len(opts.IncludeNamespaces) != len(tt.expectedNS) {
t.Errorf("expected namespaces %v, got %v", tt.expectedNS, opts.IncludeNamespaces)
}
for i, ns := range tt.expectedNS {
if i >= len(opts.IncludeNamespaces) || opts.IncludeNamespaces[i] != ns {
t.Errorf("expected namespace[%d]=%q, got %v", i, ns, opts.IncludeNamespaces)
}
}
})
}
}
func TestGetOutputTable(t *testing.T) {
items := []unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "velero.io/v1",
"kind": "Backup",
"metadata": map[string]interface{}{
"name": "test-backup-1",
"namespace": "openshift-adp",
"creationTimestamp": time.Now().Add(-2 * time.Hour).Format(time.RFC3339),
},
"status": map[string]interface{}{
"phase": "Completed",
},
},
},
{
Object: map[string]interface{}{
"apiVersion": "velero.io/v1",
"kind": "Backup",
"metadata": map[string]interface{}{
"name": "test-backup-2",
"namespace": "openshift-adp",
"creationTimestamp": time.Now().Add(-48 * time.Hour).Format(time.RFC3339),
},
"status": map[string]interface{}{
"phase": "Failed",
},
},
},
}
var buf bytes.Buffer
err := outputTable(&buf, items, "Backup")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
output := buf.String()
if !bytes.Contains([]byte(output), []byte("NAME")) {
t.Error("table output should contain NAME header")
}
if !bytes.Contains([]byte(output), []byte("test-backup-1")) {
t.Error("table output should contain test-backup-1")
}
if !bytes.Contains([]byte(output), []byte("Completed")) {
t.Error("table output should contain Completed status")
}
}
func TestDestroyOptions(t *testing.T) {
ctx := context.Background()
scheme := runtime.NewScheme()
backup := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "velero.io/v1",
"kind": "Backup",
"metadata": map[string]interface{}{
"name": "to-delete",
"namespace": "openshift-adp",
},
"spec": map[string]interface{}{},
},
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(backup).
Build()
opts := &DestroyOptions{
Name: "to-delete",
OADPNamespace: "openshift-adp",
Log: log.Log,
Client: fakeClient,
}
err := opts.runDestroy(ctx, "Backup")
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
}
func TestDestroyOptionsNotFound(t *testing.T) {
ctx := context.Background()
scheme := runtime.NewScheme()
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
Build()
opts := &DestroyOptions{
Name: "nonexistent",
OADPNamespace: "openshift-adp",
Log: log.Log,
Client: fakeClient,
}
err := opts.runDestroy(ctx, "Backup")
if err == nil {
t.Fatal("expected error for nonexistent backup, got none")
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/oadp/lifecycle_test.go` around lines 18 - 308, Tests use raw testing
assertions (t.Errorf/t.Fatalf/t.Error/t.Fatal); update all five tests
(TestVerifyBackup, TestAutoIncludeAgentNamespace, TestGetOutputTable,
TestDestroyOptions, TestDestroyOptionsNotFound) to use Gomega matchers. For each
test create g := NewWithT(t) and replace error checks with
g.Expect(err).To(HaveOccurred()) or g.Expect(err).NotTo(HaveOccurred()), replace
equality/length/contain checks with g.Expect(...).To(Equal(...)),
g.Expect(...).To(HaveLen(...)),
g.Expect(stringOutput).To(ContainSubstring("...")), and assert boolean
conditions with g.Expect(...).To(BeTrue()/BeFalse()); also replace manual
map/slice existence checks in TestVerifyBackup/TestAutoIncludeAgentNamespace
with appropriate Expect assertions. Add the gomega import (NewWithT, matchers)
and remove t.* assertion calls.

Comment thread cmd/oadp/restore.go
Comment on lines +202 to +222
// Step 3.5: Run backup integrity verification if --verify is set
if o.VerifyBackup && o.BackupName != "" && o.Client != nil {
o.Log.Info("Running backup integrity verification...")
results, verifyErr := VerifyBackup(ctx, o.Client, o.BackupName, o.OADPNamespace, o.Log)
if verifyErr != nil {
return fmt.Errorf("backup verification failed: %w", verifyErr)
}
allPassed := true
for _, r := range results {
if r.Passed {
o.Log.Info("Backup verification", "check", r.Check, "result", "pass", "detail", r.Detail)
} else {
o.Log.Info("Backup verification", "check", r.Check, "result", "FAIL", "detail", r.Detail)
allPassed = false
}
}
if !allPassed {
return fmt.Errorf("backup verification failed for '%s' — restore aborted", o.BackupName)
}
o.Log.Info("Backup verification passed, proceeding with restore")
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Verification silently skipped with --from-schedule.

When a user specifies --verify --from-schedule schedule-name, the verification step is silently skipped because BackupName is empty (line 203 condition). The user explicitly requested verification but receives no indication that it didn't run. This could create a false sense of security.

Recommend either:

  1. Add flag validation to make --verify and --from-schedule mutually exclusive, or
  2. Log a warning when --verify is set but skipped due to schedule-based restore
Option 1: Mutual exclusion validation

Add validation in the RunE function after line 70:

 			if err := opts.validateEtcdSnapshotFlags(changedFlags); err != nil {
 				return err
 			}
+			if opts.VerifyBackup && opts.ScheduleName != "" {
+				return fmt.Errorf("--verify cannot be used with --from-schedule (schedule-based restores don't reference a specific backup name)")
+			}
 			return opts.RunRestore(cmd.Context())
Option 2: Warning when verification is skipped

Add a warning log after line 195:

 	}
 
+	// Warn if verification was requested but will be skipped
+	if o.VerifyBackup && o.ScheduleName != "" {
+		o.Log.Info("Warning: --verify flag has no effect with --from-schedule (schedule-based restores select the latest backup at runtime)")
+	}
+
 	// Step 3: Generate restore object
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/oadp/restore.go` around lines 202 - 222, Validate and handle the case
where verification was requested but no explicit backup name was provided:
inside the RunE entry point, add a check for o.VerifyBackup combined with a
schedule-based restore (i.e., when o.BackupName is empty or a boolean/field
representing --from-schedule is set) and either return a user-facing error
(making --verify and --from-schedule mutually exclusive) or emit a clear warning
before proceeding; update the logic that currently checks o.VerifyBackup &&
o.BackupName != "" in the restore flow (the block using VerifyBackup(ctx,
o.Client, o.BackupName, o.OADPNamespace, o.Log)) so it cannot be silently
skipped when the user requested verification (reference o.VerifyBackup,
o.BackupName, RunE and the VerifyBackup call).

@openshift-ci
Copy link
Copy Markdown
Contributor

openshift-ci Bot commented Jun 5, 2026

@dpateriya: all tests passed!

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@hypershift-jira-solve-ci
Copy link
Copy Markdown

hypershift-jira-solve-ci Bot commented Jun 5, 2026

Now I have all the details from all four failing checks. Let me compile the final report.

Test Failure Analysis Complete

Job Information

Test Failure Analysis

Error

1. LINT — cmd/oadp/lifecycle_test.go:136:1: File is not properly formatted (gci)
2. LINT — cmd/oadp/restore.go:108:1: cyclomatic complexity 37 of func (*CreateOptions).RunRestore is high (> 30) (gocyclo)
3. VERIFY — cmd/oadp/lifecycle_test.go: needs update (go fmt produces diff on committed file)
4. CODECOV/PATCH — Patch coverage 23.71% is below target 41.43% (370 lines missing coverage)
5. CODECOV/PROJECT — Project coverage dropped -0.10% (41.33% vs 41.43% base)

Summary

All four failures stem from newly added OADP (OpenShift API for Data Protection) CLI code in PR #8685. The lint job catches two issues: cmd/oadp/lifecycle_test.go has incorrectly ordered imports (gci linter) and cmd/oadp/restore.go's RunRestore function has cyclomatic complexity of 37, exceeding the 30 threshold. The verify job fails because lifecycle_test.go was committed without running go fmt, so the verify pipeline's make fmt step modifies the file and the subsequent git diff check fails. Both codecov checks fail because the 477 new lines of code across 9 files are only 23.71% covered by tests—well below the 41.43% target—with several files at 0% coverage.

Root Cause

Four distinct issues, all in newly added code:

  1. Import ordering (gci lint failure): cmd/oadp/lifecycle_test.go line 136 has imports that don't conform to the project's import grouping rules enforced by the gci linter. The imports need to be reordered into the standard groups (stdlib, external, internal).

  2. Cyclomatic complexity (gocyclo lint failure): cmd/oadp/restore.go line 108 defines (*CreateOptions).RunRestore with a cyclomatic complexity of 37, exceeding the project maximum of 30. The function has too many branching paths (if/else chains, error checks, switch cases) and needs to be decomposed into smaller helper functions.

  3. Go formatting (verify failure): cmd/oadp/lifecycle_test.go was committed without running go fmt. The verify pipeline runs make fmt (which calls go fmt ./...), detects the file was modified, and fails the git diff --exit-code check. This is the same file flagged by gci—fixing the imports via gci and running go fmt would resolve both.

  4. Insufficient test coverage (codecov failures): The PR adds 477 lines across 9 files but only 108 are covered by tests (23.71% patch coverage vs. 41.43% required). The worst offenders are cmd/oadp/get.go (11.34%, 172 lines missing), cmd/oadp/destroy.go (14.86%, 63 lines missing), and cmd/oadp/restore.go (0%). Several command wiring files (cmd/get/get.go, cmd/verify/verify.go, cmd/destroy/destroy.go, cmd/oadp/backup.go) have 0% coverage. Only support/oadp/validate.go (87.50%) and cmd/oadp/schedule.go meet acceptable coverage.

Recommendations
  1. Fix import ordering & formatting: Run make fmt and ensure imports in cmd/oadp/lifecycle_test.go follow the project's gci configuration (stdlib → external → internal grouping). Alternatively run golangci-lint run --fix to auto-fix.

  2. Reduce RunRestore complexity: Refactor (*CreateOptions).RunRestore in cmd/oadp/restore.go to bring cyclomatic complexity from 37 to ≤30. Extract logical blocks (e.g., validation, resource creation, status polling) into dedicated helper methods.

  3. Add unit tests for OADP commands: Focus coverage efforts on the three largest gaps:

    • cmd/oadp/get.go — 171 lines uncovered (add tests for get/list logic)
    • cmd/oadp/verify_backup.go — 80 lines uncovered (add tests for backup verification paths)
    • cmd/oadp/destroy.go — 61 lines uncovered (add tests for destroy logic)
    • cmd/oadp/restore.go — 19 lines uncovered (add tests for restore paths)

    Covering these four files would add ~331 lines of coverage, bringing patch coverage well above the 41.43% threshold.

  4. Run full pre-submit checks locally before pushing: make fmt && make vet && make lint && make test

Evidence
Evidence Detail
Lint error 1 cmd/oadp/lifecycle_test.go:136:1: File is not properly formatted (gci)
Lint error 2 cmd/oadp/restore.go:108:1: cyclomatic complexity 37 of func (*CreateOptions).RunRestore is high (> 30) (gocyclo)
Verify error cmd/oadp/lifecycle_test.go: needs updatemake fmt modified file, git diff --exit-code failed
Patch coverage 23.71% actual vs 41.43% required (17.72pp gap)
Project coverage Dropped -0.10% (41.43% → 41.33%)
Coverage diff +477 lines added, +108 hits, +359 misses, +10 partials
Worst file cmd/oadp/get.go — 11.34% coverage, 171 missing + 1 partial line
Zero-coverage files cmd/oadp/restore.go, cmd/get/get.go, cmd/verify/verify.go, cmd/destroy/destroy.go, cmd/oadp/backup.go (all 0%)
Best new file support/oadp/validate.go — 87.50% coverage (1 miss, 1 partial)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/cli Indicates the PR includes changes for CLI area/control-plane-operator Indicates the PR includes changes for the control plane operator - in an OCP release area/hypershift-operator Indicates the PR includes changes for the hypershift operator and API - outside an OCP release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant