Skip to content

ci-operator: support mounting dependency images as volumes#5286

Draft
petr-muller wants to merge 1 commit into
openshift:mainfrom
petr-muller:mount-dependencies
Draft

ci-operator: support mounting dependency images as volumes#5286
petr-muller wants to merge 1 commit into
openshift:mainfrom
petr-muller:mount-dependencies

Conversation

@petr-muller

@petr-muller petr-muller commented Jun 30, 2026

Copy link
Copy Markdown
Member

Steps can now optionally set mount_path on a dependency to have the dependency image mounted as a read-only Kubernetes image volume at the specified path. This uses the ImageVolume feature (GA in OCP 4.20+/k8s 1.33+) to make dependency image contents directly accessible to the step without requiring the step to pull and extract the image itself.

ci-operator now lets CI configs attach a dependency image directly into a step container as a read-only volume by setting mount_path on step.dependencies. In practice, this means a step can consume files from a dependency image without having to pull/extract them itself, while the existing dependency environment variables still work as before.

The implementation updates multi-stage pod generation to create Kubernetes Image volumes and mounts when mount_path is provided, adds validation to ensure mount paths are absolute and don’t conflict or nest under each other, and adds tests for the new volume naming, pod wiring, and validation rules.

Steps can now optionally set mount_path on a dependency to have the
dependency image mounted as a read-only Kubernetes image volume at the
specified path. This uses the ImageVolume feature (GA in OCP 4.20+/k8s
1.33+) to make dependency image contents directly accessible to the step
without requiring the step to pull and extract the image itself.

The env var injection continues to work regardless — mount_path is
purely additive.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@openshift-merge-bot

Copy link
Copy Markdown
Contributor

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

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

This repository is configured in: automatic mode

@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 Jun 30, 2026
@openshift-ci

openshift-ci Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@openshift-ci

openshift-ci Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

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

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a MountPath field to StepDependency that causes the dependency image to be mounted as a read-only ImageVolumeSource volume at the specified path in the test container. Validation enforces that mount paths are absolute, unique, and non-nested. A dependencyVolumeName helper sanitizes dependency names for Kubernetes volume name constraints.

Changes

Dependency Image Volume Mount Support

Layer / File(s) Summary
StepDependency MountPath field
pkg/api/types.go
Adds MountPath string with json:"mount_path,omitempty" to StepDependency.
Mount path validation
pkg/validation/test.go, pkg/validation/test_test.go
validateDependencies rejects non-absolute, duplicate, and nested mount paths; test cases cover all error branches.
Pod generation: image volumes and mounts
pkg/steps/multi_stage/gen.go, pkg/steps/multi_stage/gen_test.go
envForDependencies now returns []Volume and []VolumeMount in addition to env vars; creates ImageVolumeSource with PullIfNotPresent and a read-only mount when MountPath is set; dependencyVolumeName sanitizes names to 63 chars. Tests cover name normalization and volume/mount generation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 14 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Go Error Handling ⚠️ Warning envForDependencies drops the underlying ImageDigestFor error; the new path appends a plain fmt.Errorf without %w, so the cause is lost. Wrap the lookup failure with fmt.Errorf(...: %w) or otherwise retain the original err when appending it to the aggregate.
Test Coverage For New Features ⚠️ Warning Most new behavior is tested, but the mount_path validation bug from review lacks a regression case; /var/run/images/..foo is still untested. Add a table case for the anchored parent-path edge (/var/run/images/..foo) and assert validateDependencies rejects it.
✅ Passed checks (14 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: mounting dependency images as volumes in ci-operator.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Stable And Deterministic Test Names ✅ Passed Added test/subtest names are static literals only; no Ginkgo titles or dynamic values were introduced.
Test Structure And Quality ✅ Passed New tests are table-driven unit tests with one behavior per case, clear failure messages, and no cluster setup/timeout concerns.
Microshift Test Compatibility ✅ Passed No new Ginkgo e2e tests were added; the new tests are plain testing.T unit tests and don’t use MicroShift-unsupported OpenShift APIs.
Single Node Openshift (Sno) Test Compatibility ✅ Passed No new Ginkgo e2e tests were added; the new tests are plain t.Run unit tests and contain no multi-node or SNO assumptions.
Topology-Aware Scheduling Compatibility ✅ Passed Only dependency MountPath/image-volume logic and validation changed; no replicas, affinities, node selectors, tolerations, or spread constraints were added.
Ote Binary Stdout Contract ✅ Passed No main/init/TestMain/BeforeSuite/RunSpecs stdout writes; touched code only adds generator/validator logic and unit tests.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed No new Ginkgo e2e tests were added; the changed tests are plain testing.T unit tests and show no IPv4 or external connectivity assumptions.
No-Weak-Crypto ✅ Passed Touched files only add dependency mount-path/image-volume logic; scans found no MD5/SHA1/DES/RC4/3DES/Blowfish/ECB or constant-time comparison issues.
Container-Privileges ✅ Passed No privileged/host* settings or allowPrivilegeEscalation changes were introduced; the diff only adds read-only image-volume mounts for dependencies.
No-Sensitive-Data-In-Logs ✅ Passed No new sensitive logging was added; the dependency-mount and validation changes only return errors/build pod specs, and existing logs are innocuous.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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/steps/multi_stage/gen.go (1)

425-445: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make dependency volume names collision-proof.

dependencyVolumeName() is not injective: duplicate dependency names, a-b vs a:b vs a.b, and long names with the same first 63 characters all collapse to the same volume name. validateDependencies() does not reject that, so a valid dependency list can generate duplicate pod.Spec.Volumes[*].Name values and fail pod admission.

Proposed fix
-	for _, dependency := range step.Dependencies {
+	for i, dependency := range step.Dependencies {
 		var ref string
@@
 		if dependency.MountPath != "" {
-			volName := dependencyVolumeName(dependency.Name)
+			volName := dependencyVolumeName(dependency.Name, i)
 			volumes = append(volumes, coreapi.Volume{
 				Name: volName,
@@
-func dependencyVolumeName(name string) string {
+func dependencyVolumeName(name string, index int) string {
 	sanitized := strings.NewReplacer(":", "-", ".", "-").Replace(name)
-	volName := fmt.Sprintf("dep-%s", sanitized)
-	if len(volName) > 63 {
-		volName = volName[:63]
-	}
-	return volName
+	suffix := fmt.Sprintf("-%d", index)
+	maxBaseLen := 63 - len("dep-") - len(suffix)
+	if len(sanitized) > maxBaseLen {
+		sanitized = sanitized[:maxBaseLen]
+	}
+	return fmt.Sprintf("dep-%s%s", sanitized, suffix)
 }

Also applies to: 464-470

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

In `@pkg/steps/multi_stage/gen.go` around lines 425 - 445, The volume-name
generation in dependency handling is collision-prone because
dependencyVolumeName() can map different dependency names to the same pod volume
name, and validateDependencies() does not currently catch this. Update the logic
around validateDependencies() and the volume creation path in gen.go to detect
duplicate/aliasing names up front and/or make dependencyVolumeName() produce
unique, collision-proof names for every dependency in a step, including cases
with separators and long shared prefixes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/validation/test.go`:
- Around line 967-980: The dependency mount validation in the relative-path
checks is using a deny-list style string contains test, which can miss nested
paths whose first segment starts with “..”. Update the logic around the
filepath.Rel checks in the dependency validation block to use an anchored
parent-path check based on path segments or a normalized absolute-prefix
comparison instead of strings.Contains(relPath, ".."). Keep the existing error
handling in the same validation flow and apply the fix symmetrically for both
directions of the dependency pair.

---

Outside diff comments:
In `@pkg/steps/multi_stage/gen.go`:
- Around line 425-445: The volume-name generation in dependency handling is
collision-prone because dependencyVolumeName() can map different dependency
names to the same pod volume name, and validateDependencies() does not currently
catch this. Update the logic around validateDependencies() and the volume
creation path in gen.go to detect duplicate/aliasing names up front and/or make
dependencyVolumeName() produce unique, collision-proof names for every
dependency in a step, including cases with separators and long shared prefixes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Enterprise

Run ID: 6f730622-1f82-486e-a574-7ee07e762727

📥 Commits

Reviewing files that changed from the base of the PR and between 803b3f9 and 9022853.

⛔ Files ignored due to path filters (1)
  • pkg/webreg/zz_generated.ci_operator_reference.go is excluded by !**/zz_generated*
📒 Files selected for processing (5)
  • pkg/api/types.go
  • pkg/steps/multi_stage/gen.go
  • pkg/steps/multi_stage/gen_test.go
  • pkg/validation/test.go
  • pkg/validation/test_test.go
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • openshift/release (manual)
  • openshift/ci-docs (manual)
  • openshift/release-controller (manual)
  • openshift/ci-chat-bot (manual)

Comment thread pkg/validation/test.go
Comment on lines +967 to +980
relPath, err := filepath.Rel(other.MountPath, dependency.MountPath)
if err != nil {
errs = append(errs, fmt.Errorf("%s.dependencies[%d] could not check relative path to dependencies[%d] (%w)", fieldRoot, i, index, err))
continue
}
if !strings.Contains(relPath, "..") {
errs = append(errs, fmt.Errorf("%s.dependencies[%d] mounts at %s, which is under dependencies[%d] (%s)", fieldRoot, i, dependency.MountPath, index, other.MountPath))
}
relPath, err = filepath.Rel(dependency.MountPath, other.MountPath)
if err != nil {
errs = append(errs, fmt.Errorf("%s.dependencies[%d] could not check relative path to dependencies[%d] (%w)", fieldRoot, index, i, err))
continue
}
if !strings.Contains(relPath, "..") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use an anchored parent-path check here.

strings.Contains(relPath, "..") misses child paths whose first segment merely starts with .. (for example /var/run/images/..foo). filepath.Rel("/var/run/images", "/var/run/images/..foo") returns ..foo, so this invalid nested mount currently passes validation.

Proposed fix
+				dependencyMountPath := filepath.Clean(dependency.MountPath)
+				otherMountPath := filepath.Clean(other.MountPath)
-				relPath, err := filepath.Rel(other.MountPath, dependency.MountPath)
+				relPath, err := filepath.Rel(otherMountPath, dependencyMountPath)
 				if err != nil {
 					errs = append(errs, fmt.Errorf("%s.dependencies[%d] could not check relative path to dependencies[%d] (%w)", fieldRoot, i, index, err))
 					continue
 				}
-				if !strings.Contains(relPath, "..") {
+				if relPath == "." || (relPath != ".." && !strings.HasPrefix(relPath, ".."+string(filepath.Separator))) {
 					errs = append(errs, fmt.Errorf("%s.dependencies[%d] mounts at %s, which is under dependencies[%d] (%s)", fieldRoot, i, dependency.MountPath, index, other.MountPath))
 				}
-				relPath, err = filepath.Rel(dependency.MountPath, other.MountPath)
+				relPath, err = filepath.Rel(dependencyMountPath, otherMountPath)
 				if err != nil {
 					errs = append(errs, fmt.Errorf("%s.dependencies[%d] could not check relative path to dependencies[%d] (%w)", fieldRoot, index, i, err))
 					continue
 				}
-				if !strings.Contains(relPath, "..") {
+				if relPath == "." || (relPath != ".." && !strings.HasPrefix(relPath, ".."+string(filepath.Separator))) {
 					errs = append(errs, fmt.Errorf("%s.dependencies[%d] mounts at %s, which is under dependencies[%d] (%s)", fieldRoot, index, other.MountPath, i, dependency.MountPath))
 				}

As per path instructions, "Validate at trust boundaries with allow-lists, not deny-lists".

📝 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
relPath, err := filepath.Rel(other.MountPath, dependency.MountPath)
if err != nil {
errs = append(errs, fmt.Errorf("%s.dependencies[%d] could not check relative path to dependencies[%d] (%w)", fieldRoot, i, index, err))
continue
}
if !strings.Contains(relPath, "..") {
errs = append(errs, fmt.Errorf("%s.dependencies[%d] mounts at %s, which is under dependencies[%d] (%s)", fieldRoot, i, dependency.MountPath, index, other.MountPath))
}
relPath, err = filepath.Rel(dependency.MountPath, other.MountPath)
if err != nil {
errs = append(errs, fmt.Errorf("%s.dependencies[%d] could not check relative path to dependencies[%d] (%w)", fieldRoot, index, i, err))
continue
}
if !strings.Contains(relPath, "..") {
dependencyMountPath := filepath.Clean(dependency.MountPath)
otherMountPath := filepath.Clean(other.MountPath)
relPath, err := filepath.Rel(otherMountPath, dependencyMountPath)
if err != nil {
errs = append(errs, fmt.Errorf("%s.dependencies[%d] could not check relative path to dependencies[%d] (%w)", fieldRoot, i, index, err))
continue
}
if relPath == "." || (relPath != ".." && !strings.HasPrefix(relPath, ".."+string(filepath.Separator))) {
errs = append(errs, fmt.Errorf("%s.dependencies[%d] mounts at %s, which is under dependencies[%d] (%s)", fieldRoot, i, dependency.MountPath, index, other.MountPath))
}
relPath, err = filepath.Rel(dependencyMountPath, otherMountPath)
if err != nil {
errs = append(errs, fmt.Errorf("%s.dependencies[%d] could not check relative path to dependencies[%d] (%w)", fieldRoot, index, i, err))
continue
}
if relPath == "." || (relPath != ".." && !strings.HasPrefix(relPath, ".."+string(filepath.Separator))) {
errs = append(errs, fmt.Errorf("%s.dependencies[%d] mounts at %s, which is under dependencies[%d] (%s)", fieldRoot, index, other.MountPath, i, dependency.MountPath))
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/validation/test.go` around lines 967 - 980, The dependency mount
validation in the relative-path checks is using a deny-list style string
contains test, which can miss nested paths whose first segment starts with “..”.
Update the logic around the filepath.Rel checks in the dependency validation
block to use an anchored parent-path check based on path segments or a
normalized absolute-prefix comparison instead of strings.Contains(relPath,
".."). Keep the existing error handling in the same validation flow and apply
the fix symmetrically for both directions of the dependency pair.

Source: Path instructions

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

Labels

do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant