Skip to content

Allow adding/removing manifests#485

Merged
clyang82 merged 5 commits intoopenshift-online:mainfrom
clyang82:allow_remove_manifest
Feb 11, 2026
Merged

Allow adding/removing manifests#485
clyang82 merged 5 commits intoopenshift-online:mainfrom
clyang82:allow_remove_manifest

Conversation

@clyang82
Copy link
Contributor

Files modified:

  • pkg/services/validation.go - Removed ValidateObjectUpdate function
  • pkg/services/validation_test.go - Added comprehensive test coverage
  • test/e2e/pkg/resources_test.go - Added E2E tests for adding/removing manifests

Test coverage added:

  • 10 new test cases for TestValidateManifestBundle covering edge cases like empty arrays, invalid structures, missing fields, and forbidden metadata
  • 10 new test cases for TestValidateUpdateManifestBundle including tests for different manifest counts (which now pass after removing the
    restriction)
  • E2E tests for updating workloads to add or remove manifests

Status: All changes have been committed with signoff and pushed to origin/allow_remove_manifest ✓

clyang82 and others added 2 commits February 10, 2026 21:54
Signed-off-by: clyang82 <chuyang@redhat.com>
Added test coverage for ValidateManifestBundle and ValidateManifestBundleUpdate:
- Tests for different number of manifests between new and old bundles
- Tests for empty manifests arrays
- Tests for decoding failures and invalid manifest structures
- Tests for missing required fields and forbidden metadata fields
- E2E tests for adding and removing manifests from workloads

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: clyang82 <chuyang@redhat.com>
@clyang82 clyang82 changed the title Allow remove manifest Allow adding/removing manifests Feb 10, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

Walkthrough

Replaced per-object immutability and per-update bundle comparison with a manifest metadata extraction and duplicate-detection pass during manifest-bundle validation; tests updated to new validation semantics; added E2E tests that patch ManifestWork to add, remove, and replace manifests.

Changes

Cohort / File(s) Summary
Validation implementation
pkg/services/validation.go
Removed per-object immutability and old per-update comparison logic; added manifestInfo type, extractManifestInfo, and a duplicate-detection pass that enforces uniqueness by apiVersion/kind/namespace/name.
Validation unit tests
pkg/services/validation_test.go
Reworked test cases: removed update-path tests, added/renamed cases for single- and empty-manifest bundles, updated expected error messages and whitespace-trimmed comparisons.
Resource update flow
pkg/services/resource.go
Changed update validation to call ValidateManifestBundle(resource.Payload) (validates new payload only) instead of the prior update-specific ValidateManifestBundleUpdate(...).
End-to-end manifest update tests
test/e2e/pkg/resources_test.go
Added E2E context "Update workload to add or remove manifests", tests that patch a ManifestWork to add, remove, and replace manifests, introduced newSecretManifest helper, and fixed a test context name.
Other / module context
go.mod
No functional changes to module declarations (context-only entry).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Allow adding/removing manifests' directly reflects the main objective of the PR, which is to enable the ability to add or remove manifests from workloads.
Description check ✅ Passed The description provides a clear overview of the files modified, test coverage added, and acknowledges the key change of removing restrictions on manifest counts, which aligns with the changeset.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
pkg/services/validation.go (1)

105-122: Optional: simplify extractManifestInfo signature.

This helper never returns an error, so you can drop the error return to reduce call-site noise (and update the call accordingly).

♻️ Possible simplification
-        info, err := extractManifestInfo(manifest)
-        if err != nil {
-            return fmt.Errorf("failed to extract metadata from manifest at index %d: %w", i, err)
-        }
+        info := extractManifestInfo(manifest)
...
-func extractManifestInfo(manifest datatypes.JSONMap) (*manifestInfo, error) {
+func extractManifestInfo(manifest datatypes.JSONMap) *manifestInfo {
     unstructuredObj := unstructured.Unstructured{Object: manifest}
-    return &manifestInfo{
+    return &manifestInfo{
         key: fmt.Sprintf("%s/%s/%s/%s", unstructuredObj.GetAPIVersion(), unstructuredObj.GetKind(),
             unstructuredObj.GetNamespace(), unstructuredObj.GetName()),
         name:      unstructuredObj.GetName(),
         namespace: unstructuredObj.GetNamespace(),
         gvk:       fmt.Sprintf("%s.%s", unstructuredObj.GetAPIVersion(), unstructuredObj.GetKind()),
-    }, nil
+    }
 }

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

Copy link

@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: 1

🤖 Fix all issues with AI agents
In `@pkg/services/validation_test.go`:
- Around line 145-149: The test loop in TestValidateManifestBundle does not
assert that an error is present when c.expectedErrorMsg is non-empty, so update
the subtest for ValidateManifestBundle: after calling err :=
ValidateManifestBundle(c.manifest) add a check that if c.expectedErrorMsg != ""
then require err != nil (or t.Fatalf/t.Errorf) and compare
strings.TrimSpace(err.Error()) to c.expectedErrorMsg; otherwise (when
expectedErrorMsg is empty) assert err == nil. Apply the same change to the
analogous loop in TestValidateUpdateManifestBundle so both tests fail when an
expected error is missing or mismatched.
🧹 Nitpick comments (2)
test/e2e/pkg/resources_test.go (2)

420-426: Prefer Eventually over fixed sleeps for manifest count assertions.
Fixed delays can be flaky in slower clusters; polling for the expected count is more reliable (same applies to the remove test below).

✅ Suggested refactor
-			// wait for few seconds to ensure the update is finished
-			<-time.After(5 * time.Second)
-
-			updatedWork, err := sourceWorkClient.ManifestWorks(agentTestOpts.consumerName).Get(ctx, workName, metav1.GetOptions{})
-			Expect(err).ShouldNot(HaveOccurred())
-			Expect(len(updatedWork.Spec.Workload.Manifests)).To(Equal(2))
+			Eventually(func() error {
+				updatedWork, err := sourceWorkClient.ManifestWorks(agentTestOpts.consumerName).Get(ctx, workName, metav1.GetOptions{})
+				if err != nil {
+					return err
+				}
+				if len(updatedWork.Spec.Workload.Manifests) != 2 {
+					return fmt.Errorf("unexpected manifest count, expected 2, got %d", len(updatedWork.Spec.Workload.Manifests))
+				}
+				return nil
+			}, 2*time.Minute, 2*time.Second).ShouldNot(HaveOccurred())

432-435: Make the remove‑manifest spec self‑contained (or guard the precondition).
This spec implicitly depends on the prior “add” spec. Focused runs can fail because a single‑manifest workload becomes zero after slicing. Consider ensuring at least 2 manifests (or explicitly adding one) before the removal.

🛡️ Minimal guard to make the precondition explicit
 			newWork := work.DeepCopy()
+			Expect(len(newWork.Spec.Workload.Manifests)).To(BeNumerically(">=", 2))
 			// remove the last manifest in the workload
 			newWork.Spec.Workload.Manifests = newWork.Spec.Workload.Manifests[:len(newWork.Spec.Workload.Manifests)-1]

Signed-off-by: clyang82 <chuyang@redhat.com>
Copy link

@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: 1

🤖 Fix all issues with AI agents
In `@test/e2e/pkg/resources_test.go`:
- Around line 473-475: The test logs the wrong error variable when checking
json.Unmarshal: replace the current inline call so you capture the unmarshal
error (e.g. err := json.Unmarshal(updatedWork.Spec.Workload.Manifests[0].Raw,
&manifest)) and then use that err in the Expect(...).NotTo(HaveOccurred(),
"Error unmarshalling manifest: %v", err) message (or simply call
Expect(err).NotTo(HaveOccurred())). This fixes the diagnostic so failures in
json.Unmarshal report the correct error for the manifest variable.
🧹 Nitpick comments (3)
test/e2e/pkg/resources_test.go (3)

392-393: Consider using Eventually for more reliable wait conditions.

Using a fixed time.After(5 * time.Second) sleep is less reliable than polling with Eventually. Other tests in this file use Eventually with specific conditions (e.g., checking if a resource exists). Consider replacing the fixed sleep with an Eventually block that verifies the work was actually created and is in the expected state.

This pattern also appears at lines 423, 446, and 469.


497-500: Ignored error from MarshalJSON and redundant Object assignment.

  1. The error from obj.MarshalJSON() is silently ignored. While unlikely to fail for this simple object, ignoring errors can hide issues during test development.

  2. Setting both manifest.Raw and manifest.RawExtension.Object differs from the existing NewManifest helper pattern which only sets Raw. When both are set, Raw takes precedence during serialization, making the Object assignment redundant.

♻️ Suggested improvement
-	objectStr, _ := obj.MarshalJSON()
+	objectStr, err := obj.MarshalJSON()
+	if err != nil {
+		panic(fmt.Sprintf("failed to marshal secret manifest: %v", err))
+	}
 	manifest := workv1.Manifest{}
 	manifest.Raw = objectStr
-	manifest.RawExtension.Object = obj
 	return manifest

408-451: Test ordering dependency is intentional but consider documenting.

These tests depend on sequential execution—test 2 ("remove") assumes test 1 ("add") succeeded, and test 3 ("replace") assumes test 2 succeeded. While this is appropriate for testing manifest lifecycle operations within the Ordered context, consider adding a brief comment at the context level explaining this intentional dependency to help future maintainers.

Comment on lines +473 to +475
manifest := map[string]interface{}{}
Expect(json.Unmarshal(updatedWork.Spec.Workload.Manifests[0].Raw, &manifest)).NotTo(HaveOccurred(), "Error unmarshalling manifest: %v", err)
Expect(manifest["kind"]).To(Equal("Secret"))
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Incorrect error variable in assertion message.

The err in the format string references the error from the Get() call on line 471, not the error from json.Unmarshal. If unmarshalling fails, the diagnostic message will show nil (since Get succeeded) instead of the actual unmarshal error.

🐛 Proposed fix
 			manifest := map[string]interface{}{}
-			Expect(json.Unmarshal(updatedWork.Spec.Workload.Manifests[0].Raw, &manifest)).NotTo(HaveOccurred(), "Error unmarshalling manifest:  %v", err)
+			err = json.Unmarshal(updatedWork.Spec.Workload.Manifests[0].Raw, &manifest)
+			Expect(err).NotTo(HaveOccurred(), "Error unmarshalling manifest: %v", err)
 			Expect(manifest["kind"]).To(Equal("Secret"))
📝 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
manifest := map[string]interface{}{}
Expect(json.Unmarshal(updatedWork.Spec.Workload.Manifests[0].Raw, &manifest)).NotTo(HaveOccurred(), "Error unmarshalling manifest: %v", err)
Expect(manifest["kind"]).To(Equal("Secret"))
manifest := map[string]interface{}{}
err = json.Unmarshal(updatedWork.Spec.Workload.Manifests[0].Raw, &manifest)
Expect(err).NotTo(HaveOccurred(), "Error unmarshalling manifest: %v", err)
Expect(manifest["kind"]).To(Equal("Secret"))
🤖 Prompt for AI Agents
In `@test/e2e/pkg/resources_test.go` around lines 473 - 475, The test logs the
wrong error variable when checking json.Unmarshal: replace the current inline
call so you capture the unmarshal error (e.g. err :=
json.Unmarshal(updatedWork.Spec.Workload.Manifests[0].Raw, &manifest)) and then
use that err in the Expect(...).NotTo(HaveOccurred(), "Error unmarshalling
manifest: %v", err) message (or simply call Expect(err).NotTo(HaveOccurred())).
This fixes the diagnostic so failures in json.Unmarshal report the correct error
for the manifest variable.

Adds comprehensive test coverage for duplicate manifest detection in
ValidateManifestBundleUpdate, including tests for duplicates with same
namespace, different kinds, cluster-scoped resources, and edge cases
to ensure manifests with same name but different namespace or kind are
correctly allowed.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: clyang82 <chuyang@redhat.com>
Copy link

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/services/validation.go (1)

91-117: ⚠️ Potential issue | 🔴 Critical

The old parameter is unused — tests expecting old-bundle validation will fail.

The function signature accepts both new and old manifest bundles, but the implementation only decodes and validates the new bundle. The old parameter is completely ignored.

Test cases in validation_test.go expect errors like "old manifest bundle is empty" (line 259) and "failed to decode old manifest bundle:..." (line 277), which will never be triggered with this implementation.

🔧 Proposed fix to validate the old bundle
 func ValidateManifestBundleUpdate(new, old datatypes.JSONMap) error {
 	newManifestBundleWrapper, err := api.DecodeManifestBundle(new)
 	if err != nil {
 		return fmt.Errorf("failed to decode new manifest bundle: %v", err)
 	}
 	if newManifestBundleWrapper == nil {
 		return fmt.Errorf("new manifest bundle is empty")
 	}

+	oldManifestBundleWrapper, err := api.DecodeManifestBundle(old)
+	if err != nil {
+		return fmt.Errorf("failed to decode old manifest bundle: %v", err)
+	}
+	if oldManifestBundleWrapper == nil {
+		return fmt.Errorf("old manifest bundle is empty")
+	}
+
 	// Track seen manifests to detect duplicates
 	seen := sets.New[string]()
🤖 Fix all issues with AI agents
In `@pkg/services/validation_test.go`:
- Around line 296-300: The test case name string is misleading—update the table
entry where name is set (the case with newPayload containing two apps/v1
Deployment manifests) to accurately reflect what it tests; e.g., change the name
from "duplicate manifests with different kinds" to "duplicate Deployment
manifests" so it matches the manifests created by the newPayload and
expectedErrorMsg in validation_test.go.
🧹 Nitpick comments (1)
pkg/services/validation.go (1)

128-137: Consider removing the unused error return.

extractManifestInfo always returns nil for the error. If no validation is intended here, simplifying the signature improves clarity.

♻️ Simplified signature
-func extractManifestInfo(manifest datatypes.JSONMap) (*manifestInfo, error) {
+func extractManifestInfo(manifest datatypes.JSONMap) *manifestInfo {
 	unstructuredObj := unstructured.Unstructured{Object: manifest}
 	return &manifestInfo{
 		key: fmt.Sprintf("%s/%s/%s/%s", unstructuredObj.GetAPIVersion(), unstructuredObj.GetKind(),
 			unstructuredObj.GetNamespace(), unstructuredObj.GetName()),
 		name:      unstructuredObj.GetName(),
 		namespace: unstructuredObj.GetNamespace(),
 		gvk:       fmt.Sprintf("%s.%s", unstructuredObj.GetAPIVersion(), unstructuredObj.GetKind()),
-	}, nil
+	}
 }

And update the call site accordingly:

-		info, err := extractManifestInfo(manifest)
-		if err != nil {
-			return fmt.Errorf("failed to extract metadata from manifest at index %d: %w", i, err)
-		}
+		info := extractManifestInfo(manifest)

Comment on lines 296 to 300
name: "duplicate manifests with different kinds",
newPayload: newPayload(t, "{\"id\":\"75479c10-b537-4261-8058-ca2e36bac384\",\"time\":\"2024-02-05T17:31:05Z\",\"type\":\"io.open-cluster-management.works.v1alpha1.manifestbundles.spec.create_request\",\"source\":\"grpc\",\"specversion\":\"1.0\",\"datacontenttype\":\"application/json\",\"resourceid\":\"c4df9ff0-bfeb-5bc6-a0ab-4c9128d698b4\",\"clustername\":\"b288a9da-8bfe-4c82-94cc-2b48e773fc46\",\"resourceversion\":1,\"data\":{\"manifests\":[{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"name\":\"nginx\",\"namespace\":\"default\"}},{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"name\":\"nginx\",\"namespace\":\"default\"}}],\"deleteOption\":{\"propagationPolicy\":\"Foreground\"}}}"),
oldManifest: newPayload(t, "{\"id\":\"266a8cd2-2fab-4e89-9bf0-a56425ebcdf8\",\"time\":\"2024-02-05T17:31:05Z\",\"type\":\"io.open-cluster-management.works.v1alpha1.manifestbundles.spec.create_request\",\"source\":\"grpc\",\"specversion\":\"1.0\",\"datacontenttype\":\"application/json\",\"resourceid\":\"c4df9ff0-bfeb-5bc6-a0ab-4c9128d698b4\",\"clustername\":\"b288a9da-8bfe-4c82-94cc-2b48e773fc46\",\"resourceversion\":1,\"data\":{\"manifests\":[{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"name\":\"nginx\",\"namespace\":\"default\"}}],\"deleteOption\":{\"propagationPolicy\":\"Foreground\"}}}"),
expectedErrorMsg: "duplicate manifest for resource default/nginx with resource type apps/v1.Deployment",
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Misleading test case name.

The test is named "duplicate manifests with different kinds" but both manifests are apps/v1 Deployment. The name should reflect that it tests duplicate detection for the same kind (e.g., "duplicate Deployment manifests").

🤖 Prompt for AI Agents
In `@pkg/services/validation_test.go` around lines 296 - 300, The test case name
string is misleading—update the table entry where name is set (the case with
newPayload containing two apps/v1 Deployment manifests) to accurately reflect
what it tests; e.g., change the name from "duplicate manifests with different
kinds" to "duplicate Deployment manifests" so it matches the manifests created
by the newPayload and expectedErrorMsg in validation_test.go.

seen := sets.New[string]()

errs := field.ErrorList{}
for i, manifest := range newManifestBundleWrapper.Manifests {
Copy link
Contributor

Choose a reason for hiding this comment

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

we may also add this check to the ValidateManifestBundle

and could we using ValidateManifestBundle instead of ValidateManifestBundleUpdate, I think they should have the same logic

Signed-off-by: clyang82 <chuyang@redhat.com>
@skeeey
Copy link
Contributor

skeeey commented Feb 11, 2026

LGTM

@clyang82 clyang82 merged commit 36077c4 into openshift-online:main Feb 11, 2026
13 checks passed
@clyang82 clyang82 deleted the allow_remove_manifest branch February 11, 2026 07:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants