Skip to content

Conversation

@JianLi-RH
Copy link
Contributor

@JianLi-RH JianLi-RH commented Jan 30, 2026

Test case: https://polarion.engineering.redhat.com/polarion/#/project/OSE/workitem?id=OCP-42543

Test it locally:

[jianl@jianl-thinkpadt14gen4 cluster-version-operator]$ _output/linux/amd64/cluster-version-operator-tests run-test "[Jira:\"Cluster Version Operator\"] cluster-version-operator should not install resources annotated with release.openshift.io/delete=true"
  Running Suite:  - /home/jianl/1_code/cluster-version-operator
  =============================================================
  Random Seed: 1769770387 - will randomize all specs

  Will run 1 of 1 specs
  ------------------------------
  [Jira:"Cluster Version Operator"] cluster-version-operator should not install resources annotated with release.openshift.io/delete=true [Conformance, High, 42543]
  /home/jianl/1_code/cluster-version-operator/test/cvo/cvo.go:90
    STEP: Setup ocapi.OC @ 01/30/26 18:53:07.673
  cluster-version-operator-tests "level"=0 "msg"="will use the environment timeout variable to run command: 90s"
  cluster-version-operator-tests "level"=0 "msg"="timeout is: 1m30s"
    STEP: Extract manifests @ 01/30/26 18:53:07.674
  cluster-version-operator-tests "level"=0 "msg"="Extract manifests to: /tmp/OTA-42543-manifest"
  cluster-version-operator-tests "level"=0 "msg"="the output directory does not exist, will create it: /tmp/OTA-42543-manifest"
  cluster-version-operator-tests "level"=0 "msg"="the output directory has been created: /tmp/OTA-42543-manifest"
  cluster-version-operator-tests "level"=0 "msg"="Running command succeeded." "cmd"="/usr/local/sbin/oc" "args"="adm release extract --to=/tmp/OTA-42543-manifest"
    STEP: Start to iterate all manifests @ 01/30/26 18:53:59.458
  cluster-version-operator-tests "msg"="Running command failed" "error"="exit status 1" "cmd"="/usr/local/sbin/oc" "args"="get Deployment cluster-baremetal-operator-hostedcluster -n openshift-machine-api" "output"="Error from server (NotFound): deployments.apps \"cluster-baremetal-operator-hostedcluster\" not found\n"
  cluster-version-operator-tests "msg"="Running command failed" "error"="exit status 1" "cmd"="/usr/local/sbin/oc" "args"="get Service controller-manager-service -n openshift-cloud-credential-operator" "output"="Error from server (NotFound): services \"controller-manager-service\" not found\n"
  cluster-version-operator-tests "msg"="Running command failed" "error"="exit status 1" "cmd"="/usr/local/sbin/oc" "args"="get ClusterRoleBinding default-account-cluster-network-operator" "output"="Error from server (NotFound): clusterrolebindings.rbac.authorization.k8s.io \"default-account-cluster-network-operator\" not found\n"
  cluster-version-operator-tests "msg"="Running command failed" "error"="exit status 1" "cmd"="/usr/local/sbin/oc" "args"="get PrometheusRule authentication-operator -n openshift-authentication-operator" "output"="Error from server (NotFound): prometheusrules.monitoring.coreos.com \"authentication-operator\" not found\n"
  cluster-version-operator-tests "msg"="Running command failed" "error"="exit status 1" "cmd"="/usr/local/sbin/oc" "args"="get ClusterRoleBinding default-account-openshift-machine-config-operator" "output"="Error from server (NotFound): clusterrolebindings.rbac.authorization.k8s.io \"default-account-openshift-machine-config-operator\" not found\n"
  cluster-version-operator-tests "msg"="Running command failed" "error"="exit status 1" "cmd"="/usr/local/sbin/oc" "args"="get CronJob machine-config-nodes-crd-cleanup -n openshift-machine-config-operator" "output"="Error from server (NotFound): cronjobs.batch \"machine-config-nodes-crd-cleanup\" not found\n"
  • [60.920 seconds]
  ------------------------------

  Ran 1 of 1 Specs in 60.921 seconds
  SUCCESS! -- 1 Passed | 0 Failed | 0 Pending | 0 Skipped
[
  {
    "name": "[Jira:\"Cluster Version Operator\"] cluster-version-operator should not install resources annotated with release.openshift.io/delete=true",
    "lifecycle": "blocking",
    "duration": 60920,
    "startTime": "2026-01-30 10:53:07.670976 UTC",
    "endTime": "2026-01-30 10:54:08.591888 UTC",
    "result": "passed",
    "output": "  STEP: Setup ocapi.OC @ 01/30/26 18:53:07.673\n  STEP: Extract manifests @ 01/30/26 18:53:07.674\n  STEP: Start to iterate all manifests @ 01/30/26 18:53:59.458\n"
  }
]
[jianl@jianl-thinkpadt14gen4 cluster-version-operator]$ 

/cc @hongkailiu @DavidHurta

@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Jan 30, 2026
@openshift-ci-robot
Copy link
Contributor

@JianLi-RH: This pull request explicitly references no jira issue.

Details

In response to this:

Test case: https://polarion.engineering.redhat.com/polarion/#/project/OSE/workitem?id=OCP-42543

Test it locally:

[jianl@jianl-thinkpadt14gen4 cluster-version-operator]$ _output/linux/amd64/cluster-version-operator-tests run-test "[Jira:\"Cluster Version Operator\"] cluster-version-operator should not install resources annotated with release.openshift.io/delete=true"
 Running Suite:  - /home/jianl/1_code/cluster-version-operator
 =============================================================
 Random Seed: 1769767023 - will randomize all specs

 Will run 1 of 1 specs
 ------------------------------
 [Jira:"Cluster Version Operator"] cluster-version-operator should not install resources annotated with release.openshift.io/delete=true [Conformance, High, 42543]
 /home/jianl/1_code/cluster-version-operator/test/cvo/cvo.go:90
   STEP: Setup ocapi.OC @ 01/30/26 17:57:03.144
 cluster-version-operator-tests "level"=0 "msg"="will use the environment timeout variable to run command: 120s"
 cluster-version-operator-tests "level"=0 "msg"="timeout is: 2m0s"
   STEP: Extract manifests @ 01/30/26 17:57:03.144
 cluster-version-operator-tests "level"=0 "msg"="Extract manifests to: /tmp/OTA-42543-manifest"
 cluster-version-operator-tests "level"=0 "msg"="the output directory does not exist, will create it: /tmp/OTA-42543-manifest"
 cluster-version-operator-tests "level"=0 "msg"="the output directory has been created: /tmp/OTA-42543-manifest"
 cluster-version-operator-tests "level"=0 "msg"="Running command succeeded." "cmd"="/usr/local/sbin/oc" "args"="adm release extract --to=/tmp/OTA-42543-manifest"
   STEP: Start to iterate all manifests @ 01/30/26 17:57:33.498
 cluster-version-operator-tests "msg"="Running command failed" "error"="exit status 1" "cmd"="/usr/local/sbin/oc" "args"="get Deployment cluster-baremetal-operator-hostedcluster -n openshift-machine-api" "output"="Error from server (NotFound): deployments.apps \"cluster-baremetal-operator-hostedcluster\" not found\n"
 cluster-version-operator-tests "msg"="running command failed" "error"="exit status 1" "output"="Error from server (NotFound): deployments.apps \"cluster-baremetal-operator-hostedcluster\" not found\n"
 cluster-version-operator-tests "msg"="Running command failed" "error"="exit status 1" "cmd"="/usr/local/sbin/oc" "args"="get Service controller-manager-service -n openshift-cloud-credential-operator" "output"="Error from server (NotFound): services \"controller-manager-service\" not found\n"
 cluster-version-operator-tests "msg"="running command failed" "error"="exit status 1" "output"="Error from server (NotFound): services \"controller-manager-service\" not found\n"
 cluster-version-operator-tests "msg"="Running command failed" "error"="exit status 1" "cmd"="/usr/local/sbin/oc" "args"="get ClusterRoleBinding default-account-cluster-network-operator" "output"="Error from server (NotFound): clusterrolebindings.rbac.authorization.k8s.io \"default-account-cluster-network-operator\" not found\n"
 cluster-version-operator-tests "msg"="running command failed" "error"="exit status 1" "output"="Error from server (NotFound): clusterrolebindings.rbac.authorization.k8s.io \"default-account-cluster-network-operator\" not found\n"
 cluster-version-operator-tests "msg"="Running command failed" "error"="exit status 1" "cmd"="/usr/local/sbin/oc" "args"="get PrometheusRule authentication-operator -n openshift-authentication-operator" "output"="Error from server (NotFound): prometheusrules.monitoring.coreos.com \"authentication-operator\" not found\n"
 cluster-version-operator-tests "msg"="running command failed" "error"="exit status 1" "output"="Error from server (NotFound): prometheusrules.monitoring.coreos.com \"authentication-operator\" not found\n"
 cluster-version-operator-tests "msg"="Running command failed" "error"="exit status 1" "cmd"="/usr/local/sbin/oc" "args"="get ClusterRoleBinding default-account-openshift-machine-config-operator" "output"="Error from server (NotFound): clusterrolebindings.rbac.authorization.k8s.io \"default-account-openshift-machine-config-operator\" not found\n"
 cluster-version-operator-tests "msg"="running command failed" "error"="exit status 1" "output"="Error from server (NotFound): clusterrolebindings.rbac.authorization.k8s.io \"default-account-openshift-machine-config-operator\" not found\n"
 cluster-version-operator-tests "msg"="Running command failed" "error"="exit status 1" "cmd"="/usr/local/sbin/oc" "args"="get CronJob machine-config-nodes-crd-cleanup -n openshift-machine-config-operator" "output"="Error from server (NotFound): cronjobs.batch \"machine-config-nodes-crd-cleanup\" not found\n"
 cluster-version-operator-tests "msg"="running command failed" "error"="exit status 1" "output"="Error from server (NotFound): cronjobs.batch \"machine-config-nodes-crd-cleanup\" not found\n"
 • [36.390 seconds]
 ------------------------------

 Ran 1 of 1 Specs in 36.390 seconds
 SUCCESS! -- 1 Passed | 0 Failed | 0 Pending | 0 Skipped
[
 {
   "name": "[Jira:\"Cluster Version Operator\"] cluster-version-operator should not install resources annotated with release.openshift.io/delete=true",
   "lifecycle": "blocking",
   "duration": 36390,
   "startTime": "2026-01-30 09:57:03.140591 UTC",
   "endTime": "2026-01-30 09:57:39.530736 UTC",
   "result": "passed",
   "output": "  STEP: Setup ocapi.OC @ 01/30/26 17:57:03.144\n  STEP: Extract manifests @ 01/30/26 17:57:03.144\n  STEP: Start to iterate all manifests @ 01/30/26 17:57:33.498\n"
 }
]
[jianl@jianl-thinkpadt14gen4 cluster-version-operator]$ 

/cc @hongkailiu @DavidHurta

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 openshift-eng/jira-lifecycle-plugin repository.

@coderabbitai
Copy link

coderabbitai bot commented Jan 30, 2026

Walkthrough

Adds a new CVO test that asserts resources annotated release.openshift.io/delete=true are not installed, updates the OC client API and CLI executor visibility, appends a payload entry, and adds YAML decoding dependency for manifest parsing.

Changes

Cohort / File(s) Summary
Payload file
.openshift-tests-extension/openshift_payload_cluster-version-operator.json
Appended a new test payload entry for the cluster-version-operator test verifying deletion-annotated resources are not installed.
Test logic
test/cvo/cvo.go
Added test should not install resources annotated with release.openshift.io/delete=true: extracts manifests to a temp dir, iterates files (skipping cleanup), decodes multi-document YAML, filters docs with release.openshift.io/delete=true, and asserts those resources are absent via OC run calls.
OC API
test/oc/api/api.go
Added Run(args ...string) ([]byte, error) to the OC interface.
OC CLI implementation
test/oc/cli/cli.go
Changed executor method visibility from public Run to private run, added client.Run delegator, updated internal executor calls, added OC_CLI_TIMEOUT logging, and ensured output directory creation in AdmReleaseExtract.
Dependencies
go.mod
Moved gopkg.in/yaml.v3 v3.0.1 from indirect to direct require to support YAML decoding in the new test.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

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

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

@openshift-ci
Copy link
Contributor

openshift-ci bot commented Jan 30, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: JianLi-RH
Once this PR has been reviewed and has the lgtm label, please assign fao89 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

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: 2

🤖 Fix all issues with AI agents
In `@test/cvo/cvo.go`:
- Around line 99-105: Update the assertion message to accurately describe the
expectation: change the misleading message on the
o.Expect(err).NotTo(o.HaveOccurred(), ...) call so it states that manifest
extraction should succeed (or that no error is expected) when invoking
ocClient.AdmReleaseExtract with manifestDir
(ocClient.AdmReleaseExtract(manifestDir)); reference the manifest extraction
operation and use clear text like "expected manifest extraction to succeed" or
"no error expected when extracting manifests" instead of "The NotFound error
should occur when extract manifests".
- Around line 117-119: The loop opens files with os.Open and uses defer
file.Close(), which leaks descriptors until the enclosing function returns;
replace the deferred close by closing each file immediately after its processing
(call file.Close() directly at the end of the loop iteration) or move the
per-file logic into a helper function (e.g., processManifestFile(filePath) that
opens the file and defers file.Close() inside that helper) so file handles are
released promptly; update references to the file variable and remove the in-loop
defer file.Close() in the code around os.Open and file usage.
🧹 Nitpick comments (1)
test/oc/cli/cli.go (1)

118-125: Duplicate logging in Run method.

The ocExecutor.run method (lines 38-42) already logs success/error with command details. This Run wrapper adds redundant logging, resulting in double log entries for each command execution.

Consider removing the duplicate logging here since the executor already handles it:

♻️ Proposed simplification
 func (c *client) Run(args ...string) ([]byte, error) {
-	b, err := c.executor.run(args...)
-	if err != nil {
-		c.logger.Error(err, "running command failed", "output", string(b))
-	} else {
-		c.logger.Info("running command succeeded.")
-	}
-	return b, err
+	return c.executor.run(args...)
 }

test/cvo/cvo.go Outdated
Comment on lines 99 to 105
g.By("Extract manifests")
annotation := "release.openshift.io/delete"
manifestDir := ocapi.ReleaseExtractOptions{To: "/tmp/OTA-42543-manifest"}
logger.Info(fmt.Sprintf("Extract manifests to: %s", manifestDir.To))
defer func() { _ = os.RemoveAll(manifestDir.To) }()
err = ocClient.AdmReleaseExtract(manifestDir)
o.Expect(err).NotTo(o.HaveOccurred(), "The NotFound error should occur when extract manifests")
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

Minor: assertion message is misleading.

Line 105's error message says "The NotFound error should occur when extract manifests" but this assertion expects extraction to succeed (no error). The message appears to be copy-pasted from elsewhere.

📝 Suggested fix
 		err = ocClient.AdmReleaseExtract(manifestDir)
-		o.Expect(err).NotTo(o.HaveOccurred(), "The NotFound error should occur when extract manifests")
+		o.Expect(err).NotTo(o.HaveOccurred(), "Failed to extract manifests")
📝 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
g.By("Extract manifests")
annotation := "release.openshift.io/delete"
manifestDir := ocapi.ReleaseExtractOptions{To: "/tmp/OTA-42543-manifest"}
logger.Info(fmt.Sprintf("Extract manifests to: %s", manifestDir.To))
defer func() { _ = os.RemoveAll(manifestDir.To) }()
err = ocClient.AdmReleaseExtract(manifestDir)
o.Expect(err).NotTo(o.HaveOccurred(), "The NotFound error should occur when extract manifests")
g.By("Extract manifests")
annotation := "release.openshift.io/delete"
manifestDir := ocapi.ReleaseExtractOptions{To: "/tmp/OTA-42543-manifest"}
logger.Info(fmt.Sprintf("Extract manifests to: %s", manifestDir.To))
defer func() { _ = os.RemoveAll(manifestDir.To) }()
err = ocClient.AdmReleaseExtract(manifestDir)
o.Expect(err).NotTo(o.HaveOccurred(), "Failed to extract manifests")
🤖 Prompt for AI Agents
In `@test/cvo/cvo.go` around lines 99 - 105, Update the assertion message to
accurately describe the expectation: change the misleading message on the
o.Expect(err).NotTo(o.HaveOccurred(), ...) call so it states that manifest
extraction should succeed (or that no error is expected) when invoking
ocClient.AdmReleaseExtract with manifestDir
(ocClient.AdmReleaseExtract(manifestDir)); reference the manifest extraction
operation and use clear text like "expected manifest extraction to succeed" or
"no error expected when extracting manifests" instead of "The NotFound error
should occur when extract manifests".

test/cvo/cvo.go Outdated
Comment on lines 117 to 119
file, err := os.Open(filePath)
o.Expect(err).NotTo(o.HaveOccurred())
defer file.Close()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Resource leak: defer file.Close() inside loop.

The defer statement accumulates all file handles until the function returns, rather than closing each file after processing. With many manifest files, this could exhaust file descriptors.

🐛 Proposed fix: close file immediately after processing
 			filePath := filepath.Join(manifestDir.To, entry.Name())
 			file, err := os.Open(filePath)
 			o.Expect(err).NotTo(o.HaveOccurred())
-			defer file.Close()
 			decoder := yamlv3.NewDecoder(file)
 			for {
 				var doc map[string]interface{}
 				if err := decoder.Decode(&doc); err != nil {
 					if err == io.EOF {
 						break
 					}
 					continue
 				}
 				meta, _ := doc["metadata"].(map[string]interface{})
 				ann, _ := meta["annotations"].(map[string]interface{})
 				if ann == nil || ann[annotation] != "true" {
 					continue
 				}
 				kind, _ := doc["kind"].(string)
 				name, _ := meta["name"].(string)
 				namespace, _ := meta["namespace"].(string)
 				args := []string{"get", kind, name}
 				if namespace != "" {
 					args = append(args, "-n", namespace)
 				}
 				_, err := ocClient.Run(args...)
 				o.Expect(err).To(o.HaveOccurred(), "The deleted manifest should not be installed, but actually installed")
 			}
+			file.Close()
 		}

Alternatively, extract the file processing into a helper function where defer would work correctly.

📝 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
file, err := os.Open(filePath)
o.Expect(err).NotTo(o.HaveOccurred())
defer file.Close()
filePath := filepath.Join(manifestDir.To, entry.Name())
file, err := os.Open(filePath)
o.Expect(err).NotTo(o.HaveOccurred())
decoder := yamlv3.NewDecoder(file)
for {
var doc map[string]interface{}
if err := decoder.Decode(&doc); err != nil {
if err == io.EOF {
break
}
continue
}
meta, _ := doc["metadata"].(map[string]interface{})
ann, _ := meta["annotations"].(map[string]interface{})
if ann == nil || ann[annotation] != "true" {
continue
}
kind, _ := doc["kind"].(string)
name, _ := meta["name"].(string)
namespace, _ := meta["namespace"].(string)
args := []string{"get", kind, name}
if namespace != "" {
args = append(args, "-n", namespace)
}
_, err := ocClient.Run(args...)
o.Expect(err).To(o.HaveOccurred(), "The deleted manifest should not be installed, but actually installed")
}
file.Close()
}
🤖 Prompt for AI Agents
In `@test/cvo/cvo.go` around lines 117 - 119, The loop opens files with os.Open
and uses defer file.Close(), which leaks descriptors until the enclosing
function returns; replace the deferred close by closing each file immediately
after its processing (call file.Close() directly at the end of the loop
iteration) or move the per-file logic into a helper function (e.g.,
processManifestFile(filePath) that opens the file and defers file.Close() inside
that helper) so file handles are released promptly; update references to the
file variable and remove the in-loop defer file.Close() in the code around
os.Open and file usage.

@JianLi-RH JianLi-RH changed the title NO-ISSUE: OTA-1605 Automate OCP-42543 WIP NO-ISSUE: OTA-1605 Automate OCP-42543 Jan 30, 2026
@openshift-ci openshift-ci bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jan 30, 2026
@JianLi-RH JianLi-RH force-pushed the automate_42543 branch 4 times, most recently from 8d09bb9 to f175131 Compare January 30, 2026 10:57
@JianLi-RH JianLi-RH changed the title WIP NO-ISSUE: OTA-1605 Automate OCP-42543 NO-ISSUE: OTA-1605 Automate OCP-42543 Jan 30, 2026
@openshift-ci openshift-ci bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jan 30, 2026
@JianLi-RH
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 30, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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: 3

🤖 Fix all issues with AI agents
In `@test/cvo/cvo.go`:
- Around line 134-140: The YAML decode loop currently swallows decode errors
with a continue, but the test should fail instead; replace the silent continue
with an assertion that fails the test on decode errors by calling
o.Expect(err).NotTo(o.HaveOccurred(), "decode manifest %s failed", filePath)
immediately after decoder.Decode(&doc) (remove the unreachable continue) so any
decoder.Decode failure in the loop (the symbol decoder.Decode and variable doc)
causes the test to fail and reports the filePath context.
- Around line 141-145: The annotation check in the loop (variables meta, ann and
the key annotation in test/cvo/cvo.go) wrongly assumes the value is the string
"true"; change the check to handle both boolean true and the string "true" (and
treat other truthy forms if present) by retrieving v := ann[annotation] and
using a type switch or explicit type assertions to accept v == true or v ==
"true" (and skip if ann is nil or the value is absent), so resources with
unquoted YAML true are correctly detected for deletion.
- Around line 103-108: The current code uses a fixed manifestDir.To =
"/tmp/OTA-42543-manifest" which can collide in parallel runs; replace that with
a unique temp directory created via os.MkdirTemp and assign the returned path to
manifestDir.To (handle and return/log any error from MkdirTemp), keep the defer
to os.RemoveAll(manifestDir.To) for cleanup, and then call
ocClient.AdmReleaseExtract(manifestDir) as before; update the code around the
manifestDir variable, ocapi.ReleaseExtractOptions initialization, and the defer
cleanup to use the MkdirTemp-created path.

test/cvo/cvo.go Outdated
Comment on lines 103 to 108
g.By("Extract manifests")
annotation := "release.openshift.io/delete"
manifestDir := ocapi.ReleaseExtractOptions{To: "/tmp/OTA-42543-manifest"}
logger.Info(fmt.Sprintf("Extract manifests to: %s", manifestDir.To))
defer func() { _ = os.RemoveAll(manifestDir.To) }()
err = ocClient.AdmReleaseExtract(manifestDir)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and read lines 103-108
wc -l test/cvo/cvo.go
head -n 110 test/cvo/cvo.go | tail -n 15

Repository: openshift/cluster-version-operator

Length of output: 723


🏁 Script executed:

# Look for the test function and Ginkgo setup
head -n 103 test/cvo/cvo.go | tail -n 40

Repository: openshift/cluster-version-operator

Length of output: 2398


🏁 Script executed:

# Find the test structure and look for Describe block
head -n 103 test/cvo/cvo.go | head -n 50

Repository: openshift/cluster-version-operator

Length of output: 1418


🏁 Script executed:

# Check the Describe block more carefully for parallelism setup
sed -n '47,164p' test/cvo/cvo.go | head -n 50

Repository: openshift/cluster-version-operator

Length of output: 2921


🏁 Script executed:

# Check if there's a .Parallel() call on the Describe block
grep -n "\.Parallel\|Describe.*func\|BeforeSuite\|AfterSuite" test/cvo/cvo.go

Repository: openshift/cluster-version-operator

Length of output: 269


🏁 Script executed:

# Search for MkdirTemp usage in test files
rg -t go "MkdirTemp" test/

Repository: openshift/cluster-version-operator

Length of output: 60


🏁 Script executed:

# Search for temporary directory patterns in test files
rg -t go "TempDir|ioutil\.TempDir|os\.MkdirTemp" test/

Repository: openshift/cluster-version-operator

Length of output: 60


🏁 Script executed:

# Search for /tmp paths in test files
rg -t go '/tmp/' test/

Repository: openshift/cluster-version-operator

Length of output: 169


🏁 Script executed:

# Check the full test function to understand context better
sed -n '85,125p' test/cvo/cvo.go

Repository: openshift/cluster-version-operator

Length of output: 1927


Use a unique temp directory for manifest extraction.

A fixed /tmp/OTA-42543-manifest can collide in parallel test runs, causing test flakiness and data loss. Use os.MkdirTemp() to create unique, isolated directories for each test execution.

🔧 Suggested fix
-		manifestDir := ocapi.ReleaseExtractOptions{To: "/tmp/OTA-42543-manifest"}
-		logger.Info(fmt.Sprintf("Extract manifests to: %s", manifestDir.To))
-		defer func() { _ = os.RemoveAll(manifestDir.To) }()
-		err = ocClient.AdmReleaseExtract(manifestDir)
+		tempDir, err := os.MkdirTemp("", "OTA-42543-manifest-")
+		o.Expect(err).NotTo(o.HaveOccurred(), "create temp manifest dir failed")
+		manifestDir := ocapi.ReleaseExtractOptions{To: tempDir}
+		logger.Info(fmt.Sprintf("Extract manifests to: %s", manifestDir.To))
+		defer func() { _ = os.RemoveAll(manifestDir.To) }()
+		err = ocClient.AdmReleaseExtract(manifestDir)
🤖 Prompt for AI Agents
In `@test/cvo/cvo.go` around lines 103 - 108, The current code uses a fixed
manifestDir.To = "/tmp/OTA-42543-manifest" which can collide in parallel runs;
replace that with a unique temp directory created via os.MkdirTemp and assign
the returned path to manifestDir.To (handle and return/log any error from
MkdirTemp), keep the defer to os.RemoveAll(manifestDir.To) for cleanup, and then
call ocClient.AdmReleaseExtract(manifestDir) as before; update the code around
the manifestDir variable, ocapi.ReleaseExtractOptions initialization, and the
defer cleanup to use the MkdirTemp-created path.

Comment on lines +134 to +139
var doc map[string]interface{}
if err := decoder.Decode(&doc); err != nil {
if err == io.EOF {
break
}
continue
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd test/cvo && cat -n cvo.go | sed -n '120,155p'

Repository: openshift/cluster-version-operator

Length of output: 1428


🏁 Script executed:

cd test/cvo && grep -n "o.Expect" cvo.go | head -20

Repository: openshift/cluster-version-operator

Length of output: 1819


🏁 Script executed:

cd test/cvo && grep -n "decoder.Decode\|HaveOccurred" cvo.go

Repository: openshift/cluster-version-operator

Length of output: 1432


Fail the test on YAML decode errors instead of silently continuing.

Silently continuing on decode failures can skip manifests and yield false negatives. However, the suggested fix has unreachable code—continue would never execute after o.Expect().NotTo(o.HaveOccurred()) fails. Either use o.Expect(err).NotTo(o.HaveOccurred(), "decode manifest %s failed", filePath) without the continue, or handle the error differently.

🤖 Prompt for AI Agents
In `@test/cvo/cvo.go` around lines 134 - 140, The YAML decode loop currently
swallows decode errors with a continue, but the test should fail instead;
replace the silent continue with an assertion that fails the test on decode
errors by calling o.Expect(err).NotTo(o.HaveOccurred(), "decode manifest %s
failed", filePath) immediately after decoder.Decode(&doc) (remove the
unreachable continue) so any decoder.Decode failure in the loop (the symbol
decoder.Decode and variable doc) causes the test to fail and reports the
filePath context.

Comment on lines +141 to +144
meta, _ := doc["metadata"].(map[string]interface{})
ann, _ := meta["annotations"].(map[string]interface{})
if ann == nil || ann[annotation] != "true" {
continue
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the actual code at lines 141-145 in test/cvo/cvo.go
cd /tmp && find . -name "cvo.go" -path "*/test/cvo/*" 2>/dev/null | head -5

Repository: openshift/cluster-version-operator

Length of output: 60


🏁 Script executed:

# Check if the file exists and read around lines 141-145
cat -n test/cvo/cvo.go | sed -n '130,155p'

Repository: openshift/cluster-version-operator

Length of output: 1035


🏁 Script executed:

# Let's search for where annotations come from - look for YAML unmarshaling
rg -A 5 -B 5 "annotations" test/cvo/cvo.go | head -40

Repository: openshift/cluster-version-operator

Length of output: 373


🏁 Script executed:

# Search for YAML parsing in the file to understand the source of data
rg -i "yaml\|unmarshal" test/cvo/cvo.go

Repository: openshift/cluster-version-operator

Length of output: 60


🏁 Script executed:

# Check imports to understand if YAML parsing is used
head -30 test/cvo/cvo.go

Repository: openshift/cluster-version-operator

Length of output: 830


🏁 Script executed:

# Look for YAML/manifest files that might have annotations
find test -name "*.yaml" -o -name "*.yml" | head -20

Repository: openshift/cluster-version-operator

Length of output: 60


🏁 Script executed:

# Check if there are any test manifests with annotations
rg -l "annotation" test/ --type yaml 2>/dev/null || rg "annotation" test/ -A 2 -B 2 | head -30

Repository: openshift/cluster-version-operator

Length of output: 1529


🏁 Script executed:

# Look for the test data or fixtures that might show annotation usage
fd -e yaml -e yml test/

Repository: openshift/cluster-version-operator

Length of output: 403


🏁 Script executed:

# Check what annotation value this test is looking for
rg "annotation.*=" test/cvo/cvo.go -B 10 -A 10 | head -50

Repository: openshift/cluster-version-operator

Length of output: 1596


🏁 Script executed:

# Let's look at more context around the annotation checking logic
cat -n test/cvo/cvo.go | sed -n '115,160p'

Repository: openshift/cluster-version-operator

Length of output: 1887


Fix annotation comparison to handle both boolean and string values from YAML decoding.

When YAML contains unquoted true (e.g., release.openshift.io/delete: true), the decoder produces a Go boolean instead of the string "true". The current strict string comparison misses these cases, causing resources marked for deletion to skip verification.

🔧 Suggested fix
-				ann, _ := meta["annotations"].(map[string]interface{})
-				if ann == nil || ann[annotation] != "true" {
-					continue
-				}
+				ann, _ := meta["annotations"].(map[string]interface{})
+				if ann == nil {
+					continue
+				}
+				if !strings.EqualFold(fmt.Sprint(ann[annotation]), "true") {
+					continue
+				}
📝 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
meta, _ := doc["metadata"].(map[string]interface{})
ann, _ := meta["annotations"].(map[string]interface{})
if ann == nil || ann[annotation] != "true" {
continue
}
meta, _ := doc["metadata"].(map[string]interface{})
ann, _ := meta["annotations"].(map[string]interface{})
if ann == nil {
continue
}
if !strings.EqualFold(fmt.Sprint(ann[annotation]), "true") {
continue
}
🤖 Prompt for AI Agents
In `@test/cvo/cvo.go` around lines 141 - 145, The annotation check in the loop
(variables meta, ann and the key annotation in test/cvo/cvo.go) wrongly assumes
the value is the string "true"; change the check to handle both boolean true and
the string "true" (and treat other truthy forms if present) by retrieving v :=
ann[annotation] and using a type switch or explicit type assertions to accept v
== true or v == "true" (and skip if ann is nil or the value is absent), so
resources with unquoted YAML true are correctly detected for deletion.

test/cvo/cvo.go Outdated
err = ocClient.AdmReleaseExtract(manifestDir)
o.Expect(err).NotTo(o.HaveOccurred(), "extract manifests failed")

entries, err := os.ReadDir(manifestDir.To)
Copy link
Member

Choose a reason for hiding this comment

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

nit:

Suggested change
entries, err := os.ReadDir(manifestDir.To)
files, err := os.ReadDir(manifestDir.To)

test/cvo/cvo.go Outdated
Comment on lines 116 to 117
nameLower := strings.ToLower(entry.Name())
if strings.Contains(nameLower, "cleanup") {
Copy link
Member

Choose a reason for hiding this comment

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

nit: we do not have to introduce a new var and use it only once.

Suggested change
nameLower := strings.ToLower(entry.Name())
if strings.Contains(nameLower, "cleanup") {
if strings.Contains(strings.ToLower(entry.Name()), "cleanup") {

nameLower := strings.ToLower(entry.Name())
if strings.Contains(nameLower, "cleanup") {
logger.Info(fmt.Sprintf("Skipping file %s because it matches cleanup filter", entry.Name()))
continue
Copy link
Member

Choose a reason for hiding this comment

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

Even with the logging I do not see why we skip it.
Could a resource have cleanup in its file name and annotation release.openshift.io/delete=true?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, a manifest can have cleanup in its name and annotation release.openshift.io/delete=true in its content at same time.
We can ignore this situation, in my impression this object can be deployed (not entirely sure).

args = append(args, "-n", namespace)
}
_, err := ocClient.Run(args...)
o.Expect(err).To(o.HaveOccurred(), "The deleted manifest should not be installed, but actually installed")
Copy link
Member

Choose a reason for hiding this comment

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

Ha. If I understand the code correctly, you are doing the following command with oc-cli:

$ oc get <kind> <name> -n <namespace>

Because KIND|GROUP|VERSION are dynamic, it is not easy to do it via client-go (See how CVO does it). Correct?
It is really nasty, and I really do not want it but I do not have a better way.

HOWEVER, I think this should work (which is much simpler if it does) for your case:

func ParseManifests(r io.Reader) ([]Manifest, error) {

  • Parse manifests out of files in payload
  • check if a manifest.Raw contains string release.openshift.io/delete=true;
    if yes, save it to a temp file and do oc get -f command with the temp file and expect not-found error(s).

You do not need to any yaml/json parsing here. And you will get Manifest for free. GVK is also difficult to use correctly and the way you do it now might not be accurate (for example, you are using Kind only, not Version nor Group).

Let me know what you think about it or you need more clarification.

Unlike other cases, a simple shell script would do the case. But we like Go code more. 🤷

Copy link
Contributor Author

Choose a reason for hiding this comment

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

let me give a try today

type OC interface {
AdmReleaseExtract(o ReleaseExtractOptions) error
Version(o VersionOptions) (string, error)
Run(args ...string) ([]byte, error)
Copy link
Member

Choose a reason for hiding this comment

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

Get(args ...string) (string, error) should be enough for your case.

The method Run() is be for oc run command.

If the idea https://github.com/openshift/cluster-version-operator/pull/1309/changes#r2751255955 works out, I would just do

GetFileExpectNotFoundError(args ...string) (string, error) to avoid abuse of oc get.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Like I said before https://github.com/openshift/cluster-version-operator/pull/1267/changes#r2579341159
If we adding GetFileExpectNotFoundError(), Run to OC client, we have to adding them to the interface as well. This is really not a good practice for using interface.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I really don't like to implement interface for a single instance.

_, err := os.Stat(o.To)
if errors.Is(err, os.ErrNotExist) {
c.logger.Info(fmt.Sprintf("the output directory does not exist, will create it: %s", o.To))
if err = os.Mkdir(o.To, 0755); err != nil {
Copy link
Member

Choose a reason for hiding this comment

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

Let us make the directory in the case, instead of the function.
The method here just calls oc adm release extract (maybe include some logs for debugging), nothing else.

@@ -70,12 +70,13 @@ func NewOCCli(logger logr.Logger) (api.OC, error) {
timeout := 30 * time.Second
timeoutStr := os.Getenv("OC_CLI_TIMEOUT")
Copy link
Member

Choose a reason for hiding this comment

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

Is 30s too short for oc adm release extract? You want 90s for it?

In that case, let us do another function.

func NewOCCliWithTimeout(logger logr.Logger, timeout time.Duration) (api.OC, error)

and

func NewOCCli(logger logr.Logger) (api.OC, error) {
  return NewOCCli(logger, 30 * time.Second) (api.OC, error)
}

We could remove the logic about OC_CLI_TIMEOUT (i think no one is using it at the moment). I have to admit that I did not understand your request here.

Please do it in another commit. I can do it too if you want.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok, please go ahead, I will not update my code for now.
BTW, I really do not want to introduce a new function for a parameter.
Today we add NewOCCliWithTimeout for timeout, tomorrow we may add other functions.

test/cvo/cvo.go Outdated

g.It(`should not install resources annotated with release.openshift.io/delete=true`, g.Label("Conformance", "High", "42543"), func() {
// Initialize the ocapi.OC instance
g.By("Setup ocapi.OC")
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
g.By("Setup ocapi.OC")
g.By("Setting up oc")

test/cvo/cvo.go Outdated
o.Expect(err).NotTo(o.HaveOccurred(), "Unset environment variable OC_CLI_TIMEOUT failed")
}()

g.By("Extract manifests")
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
g.By("Extract manifests")
g.By("Extracting manifests in the release")

test/cvo/cvo.go Outdated

entries, err := os.ReadDir(manifestDir.To)
o.Expect(err).NotTo(o.HaveOccurred())
g.By("Start to iterate all manifests")
Copy link
Member

@hongkailiu hongkailiu Feb 1, 2026

Choose a reason for hiding this comment

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

Suggested change
g.By("Start to iterate all manifests")
g.By("Checking if getting manifests with elease.openshift.io/delete=true on the cluster led to not-found error")

@openshift-ci
Copy link
Contributor

openshift-ci bot commented Feb 1, 2026

@JianLi-RH: 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.

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/cvo/cvo.go`:
- Around line 121-160: The deferred close inside the loop captures the loop
variable file by reference and is unnecessary because file.Close() is already
called explicitly later; remove the inline defer that references closeFilePass
and the closeFilePass variable declaration, and rely on the existing explicit
err = file.Close() error handling block (keep the o.Expect checks around
file.Close()); ensure no other code paths expect closeFilePass and that
file.Close() remains invoked for each opened file.

Comment on lines +121 to +160
file, err := os.Open(filePath)
o.Expect(err).NotTo(o.HaveOccurred())
defer func() {
if !closeFilePass {
// Close the file again
if err = file.Close(); err != nil {
o.Expect(err).NotTo(o.HaveOccurred(), "close file failed")
}
}
}()
decoder := yamlv3.NewDecoder(file)
for {
var doc map[string]interface{}
if err := decoder.Decode(&doc); err != nil {
if err == io.EOF {
break
}
continue
}
meta, _ := doc["metadata"].(map[string]interface{})
ann, _ := meta["annotations"].(map[string]interface{})
if ann == nil || ann[annotation] != "true" {
continue
}
kind, _ := doc["kind"].(string)
name, _ := meta["name"].(string)
namespace, _ := meta["namespace"].(string)
args := []string{"get", kind, name}
if namespace != "" {
args = append(args, "-n", namespace)
}
_, err := ocClient.Run(args...)
o.Expect(err).To(o.HaveOccurred(), "The deleted manifest should not be installed, but actually installed")
}
// close each file
err = file.Close()
if err != nil {
closeFilePass = false
o.Expect(err).NotTo(o.HaveOccurred(), "close file failed")
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

File handle management is overly complex and still has a subtle bug.

The defer at line 123 captures file by reference in a loop—all accumulated defers will operate on the last assigned file when the function exits. The closeFilePass flag adds complexity without fixing this. Since you already call file.Close() explicitly at line 156, remove the inner defer entirely.

🐛 Proposed simplification
 			filePath := filepath.Join(manifestDir.To, manifest.Name())
 			file, err := os.Open(filePath)
 			o.Expect(err).NotTo(o.HaveOccurred())
-			defer func() {
-				if !closeFilePass {
-					// Close the file again
-					if err = file.Close(); err != nil {
-						o.Expect(err).NotTo(o.HaveOccurred(), "close file failed")
-					}
-				}
-			}()
 			decoder := yamlv3.NewDecoder(file)
 			for {
 				// ... decode loop unchanged ...
 			}
-			// close each file
-			err = file.Close()
-			if err != nil {
-				closeFilePass = false
-				o.Expect(err).NotTo(o.HaveOccurred(), "close file failed")
-			}
+			file.Close()
 		}

Also remove the closeFilePass variable declaration at line 114.

🤖 Prompt for AI Agents
In `@test/cvo/cvo.go` around lines 121 - 160, The deferred close inside the loop
captures the loop variable file by reference and is unnecessary because
file.Close() is already called explicitly later; remove the inline defer that
references closeFilePass and the closeFilePass variable declaration, and rely on
the existing explicit err = file.Close() error handling block (keep the o.Expect
checks around file.Close()); ensure no other code paths expect closeFilePass and
that file.Close() remains invoked for each opened file.

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

Labels

jira/valid-reference Indicates that this PR references a valid Jira ticket of any type.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants