Skip to content

TRT-2295: admit and extract non-payload OTE binaries#30863

Open
smg247 wants to merge 1 commit intoopenshift:mainfrom
smg247:non-payload-ote
Open

TRT-2295: admit and extract non-payload OTE binaries#30863
smg247 wants to merge 1 commit intoopenshift:mainfrom
smg247:non-payload-ote

Conversation

@smg247
Copy link
Member

@smg247 smg247 commented Mar 11, 2026

Implements the logic detailed in the enhancement to allow for discovery and extraction of out-of-payload OTE binaries.

Also adds a new subcommand to install the TestExtension CRD and apply the matching CR

Summary by CodeRabbit

Release Notes

  • New Features

    • Added extension-admission command with install-crd and create subcommands to manage extension permissions
    • Integrated automatic discovery and validation of non-payload extensions during test execution
    • Test results now include failed test cases for unpermitted extensions
  • Refactor

    • Extracted common binary extraction logic into reusable helper methods

…mmand to install the TestExtension CRD and apply the matching CR
@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Mar 11, 2026
@openshift-ci-robot
Copy link

openshift-ci-robot commented Mar 11, 2026

@smg247: This pull request references TRT-2295 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.22.0" version, but no target version was set.

Details

In response to this:

Implements the logic detailed in the enhancement to allow for discovery and extraction of out-of-payload OTE binaries.

Also adds a new subcommand to install the TestExtension CRD and apply the matching CR

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.

@openshift-ci-robot
Copy link

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: automatic mode

@coderabbitai
Copy link

coderabbitai bot commented Mar 11, 2026

Walkthrough

This change introduces a new TestExtensionAdmission-based feature for controlling non-payload binary extraction in OpenShift tests. It adds a new API group, CRD, discovery mechanism, and CLI command to permit or restrict binary extraction from specific image streams, with unpermitted extensions reported as test failures.

Changes

Cohort / File(s) Summary
Extension Admission API Group
pkg/apis/testextension/v1/doc.go, pkg/apis/testextension/v1/register.go, pkg/apis/testextension/v1/types.go
Defines new testextension.redhat.io API group with v1 version. Includes TypeMeta, ObjectMeta, Spec with Permit field (array of namespace/imagestream patterns), and Status with standard Conditions.
Extension Admission CRD and Command
pkg/cmd/openshift-tests/extension-admission/testextensionadmission-crd.yaml, pkg/cmd/openshift-tests/extension-admission/extension_admission_command.go, cmd/openshift-tests/openshift-tests.go
Adds cluster-scoped CRD manifest and Cobra command with install-crd and create subcommands to manage TestExtensionAdmission resources via oc apply.
Non-Payload Binary Admission Discovery
pkg/test/extensions/non-payload-binary-admission.go, pkg/test/extensions/binary.go
Implements discovery of TestExtensionAdmission resources to determine permitted non-payload extensions. ExtractAllTestBinaries now returns unpermitted extensions as a fourth value alongside cleanup function, binaries, and error.
Binary Extraction Provider Enhancement
pkg/test/extensions/provider.go
Adds ExtractBinaryFromImage method to extract binaries from arbitrary (non-payload) images. Introduces extractBinary helper to consolidate extraction, decompression, and caching logic with non-payload directory isolation.
Test Result Integration
pkg/test/ginkgo/cmd_runsuite.go
Adds createUnpermittedExtensionTests to generate failing JUnit test cases for unpermitted extensions discovered during extraction.
Call Site Updates
pkg/cmd/openshift-tests/images/images_command.go, pkg/cmd/openshift-tests/list/extensions.go, pkg/testsuites/standard_suites.go
Updates ExtractAllTestBinaries call sites to handle additional return value (unpermitted extensions) using blank identifier.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: admission and extraction of non-payload OTE (openshift-tests-extension) binaries, which aligns with the core objectives of implementing discovery/extraction logic and adding new subcommands.
Stable And Deterministic Test Names ✅ Passed The pull request maintains stable and deterministic test names. All Ginkgo test cases use static string constants for their names with no dynamic information that changes between runs.
Test Structure And Quality ✅ Passed This PR does not add Ginkgo test code; it includes only standard Go unit tests, making the check not applicable.

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

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

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.5.0)

Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions


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

@openshift-ci-robot
Copy link

openshift-ci-robot commented Mar 11, 2026

@smg247: This pull request references TRT-2295 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.22.0" version, but no target version was set.

Details

In response to this:

Implements the logic detailed in the enhancement to allow for discovery and extraction of out-of-payload OTE binaries.

Also adds a new subcommand to install the TestExtension CRD and apply the matching CR

Summary by CodeRabbit

Release Notes

  • New Features

  • Added extension-admission command with install-crd and create subcommands to manage extension permissions

  • Integrated automatic discovery and validation of non-payload extensions during test execution

  • Test results now include failed test cases for unpermitted extensions

  • Refactor

  • Extracted common binary extraction logic into reusable helper methods

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.

@openshift-ci
Copy link
Contributor

openshift-ci bot commented Mar 11, 2026

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: smg247

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

The pull request process is described 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 added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Mar 11, 2026
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: 6

🧹 Nitpick comments (4)
pkg/test/extensions/non-payload-binary-admission.go (1)

119-126: Replace custom isNotFound() with Kubernetes error helpers.

This fallback decides whether to suppress API errors and continue in deny-all mode. Raw substring checks are fragile; message variations or unrelated errors containing "not found" will be misclassified. Use the standard Kubernetes helpers (apierrors.IsNotFound() or kapierrs.IsNotFound()) already established throughout the codebase for discovery and dynamic-client errors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/test/extensions/non-payload-binary-admission.go` around lines 119 - 126,
The isNotFound(err error) helper currently does fragile substring checks;
replace its body to use the Kubernetes standard error helper (e.g.,
apierrors.IsNotFound or kapierrs.IsNotFound) to determine NotFound errors and
remove the manual strings.Contains checks; update imports to include the
appropriate k8s.io/apimachinery/pkg/api/errors (or the project's kapierrs alias)
and ensure all callers of isNotFound(func) continue to work with the same
signature (isNotFound) so discovery/dynamic-client errors are detected reliably.
pkg/test/ginkgo/cmd_runsuite.go (1)

180-209: LGTM - Flake pattern for visibility without blocking.

The approach of creating both a pass and fail case for the same test name is an intentional "flake" pattern that makes unpermitted extensions visible in test results without causing the overall job to fail. This aligns with the comment on line 181 and is appropriate for rolling out this feature.

One minor formatting nit on line 196:

Cleaner string formatting
-		msg := "extension binary not permitted by TestExtensionAdmission:\n"
 		for _, u := range unpermitted {
-			msg = fmt.Sprintf("%s\n  - %s/%s:%s (%s)\n", msg, u.Namespace, u.ImageStream, u.Tag, u.Component)
+			msg += fmt.Sprintf("  - %s/%s:%s (%s)\n", u.Namespace, u.ImageStream, u.Tag, u.Component)

Or use strings.Builder for better performance with multiple appends.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/test/ginkgo/cmd_runsuite.go` around lines 180 - 209, The string building
in createUnpermittedExtensionTests currently concatenates with fmt.Sprintf
repeatedly (msg variable) which is less efficient and slightly messy; replace
that with a strings.Builder (or use fmt.Fprintf to the builder) to construct the
multi-line message inside the loop that iterates over unpermitted (for _, u :=
range unpermitted) and then set FailureOutput.Output to builder.String(), and
keep the existing logrus.Warnf calls unchanged.
pkg/cmd/openshift-tests/extension-admission/extension_admission_command.go (1)

165-171: File handle closed twice.

The file is closed via defer tmpFile.Close() at line 166, and then explicitly closed at line 171. While calling Close() twice on an *os.File is safe (subsequent calls return an error that's ignored here), the deferred os.Remove(tmpFile.Name()) at line 165 will execute before the deferred Close() at line 166 due to LIFO ordering. This means the file is removed while still open (on some OSes this is fine, on Windows it may fail).

Consider removing the deferred Close() since you're explicitly closing before exec:

Suggested fix
 	tmpFile, err := os.CreateTemp("", "testextensionadmission-*.yaml")
 	if err != nil {
 		return fmt.Errorf("failed to create temp file: %w", err)
 	}
 	defer os.Remove(tmpFile.Name())
-	defer tmpFile.Close()

 	if _, err := tmpFile.Write(yamlBytes); err != nil {
+		tmpFile.Close()
 		return fmt.Errorf("failed to write YAML to temp file: %w", err)
 	}
-	tmpFile.Close()
+	if err := tmpFile.Close(); err != nil {
+		return fmt.Errorf("failed to close temp file: %w", err)
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/cmd/openshift-tests/extension-admission/extension_admission_command.go`
around lines 165 - 171, The temp file is being closed twice (defer
tmpFile.Close() and an explicit tmpFile.Close()), and defer
os.Remove(tmpFile.Name()) will run before the deferred Close due to LIFO
ordering which can cause remove-before-close issues on some OSes; remove the
deferred tmpFile.Close() and keep the explicit tmpFile.Close() after writing
(around the tmpFile.Write(yamlBytes) error handling) so the file is closed
deterministically before or after os.Remove as intended; look for symbols
tmpFile, tmpFile.Write, tmpFile.Close, os.Remove, and yamlBytes to update the
defers accordingly.
pkg/test/extensions/binary.go (1)

1040-1045: Consider adding a label selector to reduce API response size.

Listing all ImageStreamTags across all namespaces could return a large result set. If non-payload extensions are expected to have a specific label (e.g., openshift-tests-extension=true), adding a LabelSelector to the ListOptions would reduce the API response size and improve performance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/test/extensions/binary.go` around lines 1040 - 1045, The ImageStreamTags
list call currently queries all namespaces via
imageClient.ImageV1().ImageStreamTags("").List(ctx, metav1.ListOptions{}), which
can be large; update the ListOptions to include a LabelSelector (e.g.,
"openshift-tests-extension=true") so the API returns only extension
ImageStreamTags—modify the call that constructs metav1.ListOptions passed to
ImageStreamTags("").List to set LabelSelector accordingly and ensure the label
key matches the expected non-payload extension label.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/apis/testextension/v1/types.go`:
- Around line 7-18: Add the corresponding list runtime object for the CRD by
declaring a TestExtensionAdmissionList type that implements runtime.Object and
contains metav1.TypeMeta, metav1.ListMeta and an Items []TestExtensionAdmission
slice; keep the existing +genclient on TestExtensionAdmission and add the
appropriate deepcopy-gen/k8s tags (e.g.,
+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object) on the list
type so the generated client/list/watch surfaces match the CRD's listKind.

In `@pkg/cmd/openshift-tests/extension-admission/testextensionadmission-crd.yaml`:
- Around line 29-36: Add the missing MinItems constraint and tighten the items
pattern so the CRD matches the matcher contract in
pkg/apis/testextension/v1/types.go: for the "permit" array add "minItems: 1" and
change the item's "pattern" so a segment is either a single literal "*" or a
whole name (no embedded asterisks) — i.e. enforce each side of the "/" to be
either "*" or a [A-Za-z0-9\-_]+ token; update the CRD "permit" definition to
reflect this and keep the description in sync with the matcher behavior.

In `@pkg/test/extensions/binary.go`:
- Line 1036: The function parameter name in discoverNonPayloadExtensions is
misspelled as permitPatters; rename it to permitPatterns in the function
signature and update every usage inside discoverNonPayloadExtensions (and any
local variables) to use permitPatterns, and also update any callers or
references in this file that pass or refer to permitPatters (including the
symbol used around the code block near where permitPatters is referenced) so the
identifier is consistent.

In `@pkg/test/extensions/provider.go`:
- Around line 217-228: The override environment-variable key generation for
binaryPathOverride is not sanitizing ':' characters (e.g.,
namespace/imagestream:tag), so exports with that override won't work in shells;
update the sanitizer used by binaryPathOverride (the code path invoked by the
call at binaryPathOverride(imageTag, binaryPath)) to replace ':' with a safe
character (e.g., '_') when constructing the env-var name, then return the
override so TestBinary receives a usable path; ensure the same sanitized form is
used wherever the file builds or reads the override env var.
- Around line 109-113: The fast-path returns a cached file without revalidating
its architecture; update the cache-check block in provider.go to call
checkCompatibleArchitecture(finalBinPath) before returning: if
checkCompatibleArchitecture(finalBinPath) reports compatible, log and return
finalBinPath, 0, nil as before; if it reports incompatible or an error, remove
the stale finalBinPath (os.Remove) and continue with normal download/creation
flow (do not return the invalid path). Reference finalBinPath, imageTag and the
helper checkCompatibleArchitecture when making this change.
- Around line 230-237: The non-payload cache directory is keyed only by imageTag
(safeNameForDir(imageTag)), which allows tag retargeting to reuse stale
binaries; change the key to use the image's immutable identity from imageRef
(e.g., its digest/ID) when building nonPayloadDir so the cache is tied to the
exact image, not the mutable tag; update the call site where nonPayloadDir is
constructed (provider.binPath, safeNameForDir(imageTag)) to derive a stable name
from imageRef (and fall back to tag only if no digest is available) and ensure
any logging or error messages still reference the new nonPayloadDir; keep
provider.extractBinary usage unchanged.

---

Nitpick comments:
In `@pkg/cmd/openshift-tests/extension-admission/extension_admission_command.go`:
- Around line 165-171: The temp file is being closed twice (defer
tmpFile.Close() and an explicit tmpFile.Close()), and defer
os.Remove(tmpFile.Name()) will run before the deferred Close due to LIFO
ordering which can cause remove-before-close issues on some OSes; remove the
deferred tmpFile.Close() and keep the explicit tmpFile.Close() after writing
(around the tmpFile.Write(yamlBytes) error handling) so the file is closed
deterministically before or after os.Remove as intended; look for symbols
tmpFile, tmpFile.Write, tmpFile.Close, os.Remove, and yamlBytes to update the
defers accordingly.

In `@pkg/test/extensions/binary.go`:
- Around line 1040-1045: The ImageStreamTags list call currently queries all
namespaces via imageClient.ImageV1().ImageStreamTags("").List(ctx,
metav1.ListOptions{}), which can be large; update the ListOptions to include a
LabelSelector (e.g., "openshift-tests-extension=true") so the API returns only
extension ImageStreamTags—modify the call that constructs metav1.ListOptions
passed to ImageStreamTags("").List to set LabelSelector accordingly and ensure
the label key matches the expected non-payload extension label.

In `@pkg/test/extensions/non-payload-binary-admission.go`:
- Around line 119-126: The isNotFound(err error) helper currently does fragile
substring checks; replace its body to use the Kubernetes standard error helper
(e.g., apierrors.IsNotFound or kapierrs.IsNotFound) to determine NotFound errors
and remove the manual strings.Contains checks; update imports to include the
appropriate k8s.io/apimachinery/pkg/api/errors (or the project's kapierrs alias)
and ensure all callers of isNotFound(func) continue to work with the same
signature (isNotFound) so discovery/dynamic-client errors are detected reliably.

In `@pkg/test/ginkgo/cmd_runsuite.go`:
- Around line 180-209: The string building in createUnpermittedExtensionTests
currently concatenates with fmt.Sprintf repeatedly (msg variable) which is less
efficient and slightly messy; replace that with a strings.Builder (or use
fmt.Fprintf to the builder) to construct the multi-line message inside the loop
that iterates over unpermitted (for _, u := range unpermitted) and then set
FailureOutput.Output to builder.String(), and keep the existing logrus.Warnf
calls unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9fa7a282-b3f1-4e14-9581-abc5f491e3c7

📥 Commits

Reviewing files that changed from the base of the PR and between 02b4172 and c53a075.

📒 Files selected for processing (13)
  • cmd/openshift-tests/openshift-tests.go
  • pkg/apis/testextension/v1/doc.go
  • pkg/apis/testextension/v1/register.go
  • pkg/apis/testextension/v1/types.go
  • pkg/cmd/openshift-tests/extension-admission/extension_admission_command.go
  • pkg/cmd/openshift-tests/extension-admission/testextensionadmission-crd.yaml
  • pkg/cmd/openshift-tests/images/images_command.go
  • pkg/cmd/openshift-tests/list/extensions.go
  • pkg/test/extensions/binary.go
  • pkg/test/extensions/non-payload-binary-admission.go
  • pkg/test/extensions/provider.go
  • pkg/test/ginkgo/cmd_runsuite.go
  • pkg/testsuites/standard_suites.go

Comment on lines +7 to +18
// +genclient
// +genclient:nonNamespaced
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// TestExtensionAdmission controls which ImageStreams are permitted to provide extension test binaries
type TestExtensionAdmission struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec TestExtensionAdmissionSpec `json:"spec"`
Status TestExtensionAdmissionStatus `json:"status,omitempty"`
}
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:

#!/bin/bash
set -euo pipefail

# Verify whether the API package already defines the expected list type.
rg -n -C2 '^\s*// \+genclient|^\s*type TestExtensionAdmission(List)? struct' pkg/apis/testextension/v1

Repository: openshift/origin

Length of output: 774


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search the entire testextension v1 package for TestExtensionAdmissionList
echo "=== Checking for TestExtensionAdmissionList in v1 package ==="
rg -n 'TestExtensionAdmissionList' pkg/apis/testextension/v1/

echo -e "\n=== All files in testextension/v1 directory ==="
fd . pkg/apis/testextension/v1/ -type f

echo -e "\n=== Checking for any CRD definition files ==="
rg -n 'listKind.*TestExtensionAdmission' --type yaml --type json pkg/apis/testextension/ 2>/dev/null || echo "No CRD manifests found with listKind"

Repository: openshift/origin

Length of output: 121


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# List all files in the testextension/v1 package
echo "=== Files in testextension/v1 directory ==="
find pkg/apis/testextension/v1/ -type f 2>/dev/null || echo "Directory not found"

echo -e "\n=== Checking types.go for all type definitions ==="
rg '^\s*type\s+\w+\s*(struct|interface)' pkg/apis/testextension/v1/types.go || echo "No types found"

echo -e "\n=== Searching entire codebase for TestExtensionAdmissionList ==="
rg 'TestExtensionAdmissionList' . --type go 2>/dev/null | head -20 || echo "TestExtensionAdmissionList not found anywhere"

echo -e "\n=== Checking for +genclient annotations in the package ==="
rg '\+genclient' pkg/apis/testextension/v1/ || echo "No genclient annotations found"

Repository: openshift/origin

Length of output: 666


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Content of register.go ==="
cat pkg/apis/testextension/v1/register.go

echo -e "\n=== Searching for CRD manifests mentioning TestExtensionAdmission ==="
find . -type f \( -name "*.yaml" -o -name "*.yml" -o -name "*.json" \) -exec grep -l "TestExtensionAdmission" {} \; 2>/dev/null | head -10 || echo "No CRD manifests found"

Repository: openshift/origin

Length of output: 459


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== CRD manifest content ==="
cat ./pkg/cmd/openshift-tests/extension-admission/testextensionadmission-crd.yaml

Repository: openshift/origin

Length of output: 1555


Add the list runtime object before keeping +genclient here.

The CRD declares listKind: TestExtensionAdmissionList, but the corresponding Go runtime object is missing from the types. With +genclient enabled on the singular type, this mismatch will break generated list/watch client surfaces.

Suggested fix
 // +genclient
 // +genclient:nonNamespaced
 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

 // TestExtensionAdmission controls which ImageStreams are permitted to provide extension test binaries
 type TestExtensionAdmission struct {
 	metav1.TypeMeta   `json:",inline"`
 	metav1.ObjectMeta `json:"metadata,omitempty"`

 	Spec   TestExtensionAdmissionSpec   `json:"spec"`
 	Status TestExtensionAdmissionStatus `json:"status,omitempty"`
 }
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+type TestExtensionAdmissionList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []TestExtensionAdmission `json:"items"`
+}
📝 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
// +genclient
// +genclient:nonNamespaced
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// TestExtensionAdmission controls which ImageStreams are permitted to provide extension test binaries
type TestExtensionAdmission struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec TestExtensionAdmissionSpec `json:"spec"`
Status TestExtensionAdmissionStatus `json:"status,omitempty"`
}
// +genclient
// +genclient:nonNamespaced
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// TestExtensionAdmission controls which ImageStreams are permitted to provide extension test binaries
type TestExtensionAdmission struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec TestExtensionAdmissionSpec `json:"spec"`
Status TestExtensionAdmissionStatus `json:"status,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type TestExtensionAdmissionList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []TestExtensionAdmission `json:"items"`
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/apis/testextension/v1/types.go` around lines 7 - 18, Add the
corresponding list runtime object for the CRD by declaring a
TestExtensionAdmissionList type that implements runtime.Object and contains
metav1.TypeMeta, metav1.ListMeta and an Items []TestExtensionAdmission slice;
keep the existing +genclient on TestExtensionAdmission and add the appropriate
deepcopy-gen/k8s tags (e.g.,
+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object) on the list
type so the generated client/list/watch surfaces match the CRD's listKind.

Comment on lines +29 to +36
permit:
type: array
description: List of permitted ImageStream patterns in format "namespace/imagestream". Supports wildcards like "openshift/*", "*/*", or specific "namespace/stream"
items:
type: string
pattern: '^[a-zA-Z0-9\*]([a-zA-Z0-9\-\_\*]*[a-zA-Z0-9\*])?/[a-zA-Z0-9\*]([a-zA-Z0-9\-\_\*]*[a-zA-Z0-9\*])?$'
required:
- permit
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

Tighten the CRD schema to the matcher’s actual contract.

This manifest currently accepts configurations the runtime will not honor: it omits the MinItems=1 constraint from pkg/apis/testextension/v1/types.go, and the regex allows embedded * forms even though matching only treats a whole segment * as wildcard. That means values like permit: [] or ns/foo* can be admitted and then degrade into deny-all / never-match behavior.

Possible fix
                 permit:
                   type: array
+                  minItems: 1
                   description: List of permitted ImageStream patterns in format "namespace/imagestream". Supports wildcards like "openshift/*", "*/*", or specific "namespace/stream"
                   items:
                     type: string
-                    pattern: '^[a-zA-Z0-9\*]([a-zA-Z0-9\-\_\*]*[a-zA-Z0-9\*])?/[a-zA-Z0-9\*]([a-zA-Z0-9\-\_\*]*[a-zA-Z0-9\*])?$'
+                    # Allow either an exact segment or a full-segment "*" wildcard.
+                    pattern: '^(\\*|[^/*]+)/(\*|[^/*]+)$'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/cmd/openshift-tests/extension-admission/testextensionadmission-crd.yaml`
around lines 29 - 36, Add the missing MinItems constraint and tighten the items
pattern so the CRD matches the matcher contract in
pkg/apis/testextension/v1/types.go: for the "permit" array add "minItems: 1" and
change the item's "pattern" so a segment is either a single literal "*" or a
whole name (no embedded asterisks) — i.e. enforce each side of the "/" to be
either "*" or a [A-Za-z0-9\-_]+ token; update the CRD "permit" definition to
reflect this and keep the description in sync with the matcher behavior.


// discoverNonPayloadExtensions lists ImageStreamTags with ComponentAnnotation in all namespaces,
// parses binary annotation and image ref, and splits into permitted vs unpermitted by the admission result.
func discoverNonPayloadExtensions(ctx context.Context, oc *exutil.CLI, permitPatters []PermitPattern) (permitted []nonPayloadSource, unpermitted []UnpermittedExtension, err error) {
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

Typo in parameter name: permitPatterspermitPatterns.

Suggested fix
-func discoverNonPayloadExtensions(ctx context.Context, oc *exutil.CLI, permitPatters []PermitPattern) (permitted []nonPayloadSource, unpermitted []UnpermittedExtension, err error) {
-	if len(permitPatters) == 0 {
+func discoverNonPayloadExtensions(ctx context.Context, oc *exutil.CLI, permitPatterns []PermitPattern) (permitted []nonPayloadSource, unpermitted []UnpermittedExtension, err error) {
+	if len(permitPatterns) == 0 {

Also update line 1079:

-		if MatchesAnyPermit(namespace, streamName, permitPatters) {
+		if MatchesAnyPermit(namespace, streamName, permitPatterns) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/test/extensions/binary.go` at line 1036, The function parameter name in
discoverNonPayloadExtensions is misspelled as permitPatters; rename it to
permitPatterns in the function signature and update every usage inside
discoverNonPayloadExtensions (and any local variables) to use permitPatterns,
and also update any callers or references in this file that pass or refer to
permitPatters (including the symbol used around the code block near where
permitPatters is referenced) so the identifier is consistent.

Comment on lines +109 to +113
// Check if the binary already exists in cache
if _, err := os.Stat(finalBinPath); err == nil {
logrus.Infof("Using existing binary %s for %s", finalBinPath, imageTag)
return finalBinPath, 0, nil
}
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

Revalidate cached binaries before returning them.

The fast-path at Line 110 returns any existing file before checkCompatibleArchitecture runs. A cached artifact created on another host architecture, or left behind after a failed validation, will be treated as valid and only fail later when executed.

Suggested change
-	if _, err := os.Stat(finalBinPath); err == nil {
-		logrus.Infof("Using existing binary %s for %s", finalBinPath, imageTag)
-		return finalBinPath, 0, nil
-	}
+	if _, err := os.Stat(finalBinPath); err == nil {
+		if err := checkCompatibleArchitecture(finalBinPath); err != nil {
+			_ = os.Remove(finalBinPath)
+			return "", 0, errors.WithMessage(err, "cached binary has incompatible architecture")
+		}
+		logrus.Infof("Using existing binary %s for %s", finalBinPath, imageTag)
+		return finalBinPath, 0, nil
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/test/extensions/provider.go` around lines 109 - 113, The fast-path
returns a cached file without revalidating its architecture; update the
cache-check block in provider.go to call
checkCompatibleArchitecture(finalBinPath) before returning: if
checkCompatibleArchitecture(finalBinPath) reports compatible, log and return
finalBinPath, 0, nil as before; if it reports incompatible or an error, remove
the stale finalBinPath (os.Remove) and continue with normal download/creation
flow (do not return the invalid path). Reference finalBinPath, imageTag and the
helper checkCompatibleArchitecture when making this change.

Comment on lines +217 to 228
// Allow overriding image path to an already existing local path, mostly useful for development
if override := binaryPathOverride(imageTag, binaryPath); override != "" {
logrus.WithFields(logrus.Fields{
"tag": imageTag,
"binary": binaryPath,
"override": override,
}).Info("Found override for this extension")
return &TestBinary{
imageTag: imageTag,
binaryPath: override,
}, nil
}
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

Sanitize : in the override env-var key for non-payload tags.

Line 218 now passes values like namespace/imagestream:tag into binaryPathOverride, but the sanitizer later in this file does not replace :. That means the new override path won't work with normal shell export syntax in common dev workflows.

Suggested change
-	safeEnvVar := strings.NewReplacer("/", "_", "-", "_", ".", "_")
+	safeEnvVar := strings.NewReplacer("/", "_", "-", "_", ".", "_", ":", "_")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/test/extensions/provider.go` around lines 217 - 228, The override
environment-variable key generation for binaryPathOverride is not sanitizing ':'
characters (e.g., namespace/imagestream:tag), so exports with that override
won't work in shells; update the sanitizer used by binaryPathOverride (the code
path invoked by the call at binaryPathOverride(imageTag, binaryPath)) to replace
':' with a safe character (e.g., '_') when constructing the env-var name, then
return the override so TestBinary receives a usable path; ensure the same
sanitized form is used wherever the file builds or reads the override env var.

Comment on lines +230 to +237
// Create a subdirectory for non-payload binaries to keep them separate
nonPayloadDir := filepath.Join(provider.binPath, "non-payload", safeNameForDir(imageTag))
if err := createBinPath(nonPayloadDir); err != nil {
return nil, errors.WithMessagef(err, "error creating cache path %s", nonPayloadDir)
}

// Verify the binary actually exists
fileInfo, err := os.Stat(extractedBinary)
// Extract the binary using common logic
extractedBinary, extractDuration, err := provider.extractBinary(imageRef, binaryPath, nonPayloadDir, imageTag)
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

Key the non-payload cache by immutable image identity.

Line 231 uses only imageTag for the cache directory. Because tags can be retargeted while this cache survives for days, a later run can silently reuse a binary from the old image even when imageRef has changed.

Suggested change
-	nonPayloadDir := filepath.Join(provider.binPath, "non-payload", safeNameForDir(imageTag))
+	nonPayloadDir := filepath.Join(
+		provider.binPath,
+		"non-payload",
+		safeNameForDir(imageTag),
+		pullSpecToDirName(imageRef),
+	)
📝 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
// Create a subdirectory for non-payload binaries to keep them separate
nonPayloadDir := filepath.Join(provider.binPath, "non-payload", safeNameForDir(imageTag))
if err := createBinPath(nonPayloadDir); err != nil {
return nil, errors.WithMessagef(err, "error creating cache path %s", nonPayloadDir)
}
// Verify the binary actually exists
fileInfo, err := os.Stat(extractedBinary)
// Extract the binary using common logic
extractedBinary, extractDuration, err := provider.extractBinary(imageRef, binaryPath, nonPayloadDir, imageTag)
// Create a subdirectory for non-payload binaries to keep them separate
nonPayloadDir := filepath.Join(
provider.binPath,
"non-payload",
safeNameForDir(imageTag),
pullSpecToDirName(imageRef),
)
if err := createBinPath(nonPayloadDir); err != nil {
return nil, errors.WithMessagef(err, "error creating cache path %s", nonPayloadDir)
}
// Extract the binary using common logic
extractedBinary, extractDuration, err := provider.extractBinary(imageRef, binaryPath, nonPayloadDir, imageTag)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/test/extensions/provider.go` around lines 230 - 237, The non-payload
cache directory is keyed only by imageTag (safeNameForDir(imageTag)), which
allows tag retargeting to reuse stale binaries; change the key to use the
image's immutable identity from imageRef (e.g., its digest/ID) when building
nonPayloadDir so the cache is tied to the exact image, not the mutable tag;
update the call site where nonPayloadDir is constructed (provider.binPath,
safeNameForDir(imageTag)) to derive a stable name from imageRef (and fall back
to tag only if no digest is available) and ensure any logging or error messages
still reference the new nonPayloadDir; keep provider.extractBinary usage
unchanged.

@openshift-ci-robot
Copy link

Scheduling required tests:
/test e2e-aws-csi
/test e2e-aws-ovn-fips
/test e2e-aws-ovn-microshift
/test e2e-aws-ovn-microshift-serial
/test e2e-aws-ovn-serial-1of2
/test e2e-aws-ovn-serial-2of2
/test e2e-gcp-csi
/test e2e-gcp-ovn
/test e2e-gcp-ovn-upgrade
/test e2e-metal-ipi-ovn-ipv6
/test e2e-vsphere-ovn
/test e2e-vsphere-ovn-upi

@pacevedom
Copy link
Contributor

/test e2e-aws-ovn-microshift

@openshift-ci
Copy link
Contributor

openshift-ci bot commented Mar 11, 2026

@smg247: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/e2e-aws-ovn-microshift c53a075 link true /test e2e-aws-ovn-microshift

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.

@openshift-trt
Copy link

openshift-trt bot commented Mar 11, 2026

Risk analysis has seen new tests most likely introduced by this PR.
Please ensure that new tests meet guidelines for naming and stability.

New Test Risks for sha: c53a075

Job Name New Test Risk
pull-ci-openshift-origin-main-e2e-aws-ovn-microshift High - "[openshift-tests] extension binary not permitted" is a new test that was not present in all runs against the current commit.

New tests seen in this PR at sha: c53a075

  • "[openshift-tests] extension binary not permitted" [Total: 10, Pass: 10, Fail: 0, Flake: 0]

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

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. 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