Skip to content

OCPBUGS-77670: Handle incomplete workspace directory structures gracefully#1464

Open
redhat-chai-bot wants to merge 1 commit into
openshift:mainfrom
redhat-chai-bot:fix/ocpbugs-77670-workspace-integrity
Open

OCPBUGS-77670: Handle incomplete workspace directory structures gracefully#1464
redhat-chai-bot wants to merge 1 commit into
openshift:mainfrom
redhat-chai-bot:fix/ocpbugs-77670-workspace-integrity

Conversation

@redhat-chai-bot

@redhat-chai-bot redhat-chai-bot commented Jul 6, 2026

Copy link
Copy Markdown

Summary

Fixes OCPBUGS-77670 — Mirror process sometimes failing to mirror and ending abruptly.

Problem

When an oc-mirror run is interrupted (e.g. via Ctrl+C, disk full, reboot), incomplete directory structures are left in the oc-mirror-workspace/working-dir/release-images/ tree. On subsequent runs, oc-mirror encounters these incomplete directories (missing oci-layout, index.json, or blob files) and fails instead of recovering.

Expected Behavior

oc-mirror should detect incomplete workspace directory structures and self-heal by re-downloading the affected content, rather than failing the entire mirror process.

Changes

Handles incomplete workspace directory structures gracefully so that interrupted mirror operations do not leave the workspace in an unrecoverable state.

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of cached release data so incomplete or invalid directories are no longer reused.
    • Valid release directories are now kept intact, while broken ones are automatically replaced.
    • Added coverage for valid, missing, empty, and malformed release directory cases.

When oc-mirror v2 is interrupted (Ctrl+C, disk full, reboot), it can
leave incomplete OCI directories under working-dir/release-images/
that are missing critical files like oci-layout and index.json.
Subsequent runs fail because ensureReleaseInOCIFormat() only checks
if the directory exists (os.Stat) but never validates its contents.

Add isValidOCIDirectory() to verify that an OCI directory contains
both a valid oci-layout file (with imageLayoutVersion) and a valid
index.json file (with schemaVersion). Modify ensureReleaseInOCIFormat()
to detect incomplete directories, log a warning, remove the corrupt
directory, and re-download the release image.

This follows the same pattern already used in the operator catalog
handler (catalog_handler.go) for validating OCI directories.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@openshift-ci-robot openshift-ci-robot added jira/severity-important Referenced Jira bug's severity is important for the branch this PR is targeting. jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. jira/invalid-bug Indicates that a referenced Jira bug is invalid for the branch this PR is targeting. labels Jul 6, 2026
@openshift-ci-robot

Copy link
Copy Markdown

@redhat-chai-bot: This pull request references Jira Issue OCPBUGS-77670, which is invalid:

  • expected the bug to target the "5.0.0" version, but no target version was set

Comment /jira refresh to re-evaluate validity if changes to the Jira bug are made, or edit the title of this pull request to link to a different bug.

The bug has been updated to refer to the pull request using the external bug tracker.

Details

In response to this:

Summary

Fixes OCPBUGS-77670 — Mirror process sometimes failing to mirror and ending abruptly.

Problem

When an oc-mirror run is interrupted (e.g. via Ctrl+C, disk full, reboot), incomplete directory structures are left in the oc-mirror-workspace/working-dir/release-images/ tree. On subsequent runs, oc-mirror encounters these incomplete directories (missing oci-layout, index.json, or blob files) and fails instead of recovering.

Expected Behavior

oc-mirror should detect incomplete workspace directory structures and self-heal by re-downloading the affected content, rather than failing the entire mirror process.

Changes

Handles incomplete workspace directory structures gracefully so that interrupted mirror operations do not leave the workspace in an unrecoverable state.

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
openshift-ci Bot requested review from adolfo-ab and r4f4 July 6, 2026 14:13
@openshift-ci

openshift-ci Bot commented Jul 6, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: redhat-chai-bot
Once this PR has been reviewed and has the lgtm label, please assign aguidirh 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 Jul 6, 2026

Copy link
Copy Markdown

Walkthrough

Adds validation of cached OCI-format release directories in the local storage collector. A new isValidOCIDirectory helper checks oci-layout and index.json contents; invalid/incomplete directories are removed and re-populated instead of reused. Tests cover the helper and the updated ensureReleaseInOCIFormat behavior.

Changes

OCI Directory Validation

Layer / File(s) Summary
OCI validity check and reuse logic
internal/pkg/release/local_stored_collector.go
Adds encoding/json import and isValidOCIDirectory helper that checks oci-layout and index.json for valid imageLayoutVersion and schemaVersion; ensureReleaseInOCIFormat now removes and re-copies incomplete/invalid directories instead of always reusing them.
Tests for validation helper and reuse behavior
internal/pkg/release/local_stored_collector_test.go
Adds os import, createValidOCIDir test helper, TestIsValidOCIDirectory, and TestEnsureReleaseInOCIFormat_IncompleteDir covering valid, invalid, missing, and empty directory scenarios.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant ensureReleaseInOCIFormat
  participant isValidOCIDirectory
  participant Filesystem

  Caller->>ensureReleaseInOCIFormat: request release in OCI format
  ensureReleaseInOCIFormat->>Filesystem: check if target directory exists
  ensureReleaseInOCIFormat->>isValidOCIDirectory: validate oci-layout and index.json
  isValidOCIDirectory->>Filesystem: read oci-layout and index.json
  isValidOCIDirectory-->>ensureReleaseInOCIFormat: valid or invalid
  alt valid
    ensureReleaseInOCIFormat-->>Caller: reuse existing directory
  else invalid
    ensureReleaseInOCIFormat->>Filesystem: remove directory
    ensureReleaseInOCIFormat->>Filesystem: copy release index image
    ensureReleaseInOCIFormat-->>Caller: return new directory
  end
Loading

Suggested reviewers: adolfo-ab

🚥 Pre-merge checks | ✅ 15
✅ Passed checks (15 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: detecting and recovering from incomplete workspace directories.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 t.Run titles are static and deterministic; no dynamic IDs, timestamps, or generated names appear.
Test Structure And Quality ✅ Passed PASS: These are standard Go unit tests, not Ginkgo; each subtest targets one behavior, uses t.TempDir for cleanup, and has no cluster waits/timeouts.
Microshift Test Compatibility ✅ Passed No new Ginkgo e2e tests were added; the changes are unit tests in internal/pkg/release with no MicroShift-only API usage.
Single Node Openshift (Sno) Test Compatibility ✅ Passed The added tests are plain Go unit tests, not Ginkgo e2e tests, and they only exercise filesystem logic with no cluster/node assumptions.
Topology-Aware Scheduling Compatibility ✅ Passed Only OCI cache validation and filesystem tests changed; no deployment manifests/controllers, scheduling constraints, or topology-dependent node selection were added.
Ote Binary Stdout Contract ✅ Passed Touched files are library/test code only; no main/init/TestMain/RunSpecs hooks or stdout writes were added.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed The added tests are plain Go unit tests, not Ginkgo e2e tests, and they don't contain IPv4 or external-network assumptions.
No-Weak-Crypto ✅ Passed The PR only adds OCI directory validation and tests; I found no MD5/SHA1/DES/RC4/3DES/Blowfish/ECB use, custom crypto, or secret comparisons.
Container-Privileges ✅ Passed PR changes only Go code and tests; no container/K8s manifests or privilege flags were added in the touched files.
No-Sensitive-Data-In-Logs ✅ Passed The new warning logs only the OCI directory path; the changed code does not log passwords, tokens, PII, hostnames, or customer data.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

@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

🧹 Nitpick comments (1)
internal/pkg/release/local_stored_collector_test.go (1)

925-959: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Dead mirrorRun variable doesn't actually verify re-download occurred.

mirrorRun is declared, never set true, and only referenced via _ = mirrorRun to satisfy the compiler. The comment implies it proves re-download happened, but the test only checks that the directory was recreated and no error occurred — it doesn't confirm Mirror.Run was actually invoked. Compare with the other two subtests, which use MockMirror{Fail: true} to prove/disprove invocation more convincingly.

♻️ Suggested cleanup
-		mirrorRun := false
 		collector := &LocalStorageCollector{
 			Log:              log,
 			Mirror:           &MockMirror{Fail: false},
 			Opts:             mirror.CopyOptions{},
 			LocalStorageFQDN: "localhost:9999",
 			LogsDir:          "/tmp/",
 		}
@@
 		err = collector.ensureReleaseInOCIFormat(context.Background(), release, releaseDir)
 		assert.NoError(t, err)
 		// Confirm the directory was recreated (MkdirAll runs after removal)
 		_, statErr := os.Stat(releaseDir)
 		assert.NoError(t, statErr)
-		_ = mirrorRun
🤖 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 `@internal/pkg/release/local_stored_collector_test.go` around lines 925 - 959,
The “incomplete directory is removed and re-downloaded” test uses a dead
mirrorRun variable that never proves Mirror.Run was called. Update this subtest
around ensureReleaseInOCIFormat and MockMirror so it actually asserts
re-download occurred, either by using the mock’s failure/success behavior
consistently with the neighboring subtests or by recording invocation on the
mock and asserting it. Remove the unused mirrorRun placeholder and keep the
assertions focused on observable Mirror.Run execution plus the recreated
directory.
🤖 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 `@internal/pkg/release/local_stored_collector.go`:
- Around line 227-266: isValidOCIDirectory is too permissive because it only
checks that oci-layout and index.json are parseable, so incomplete OCI layouts
can still be treated as valid. Tighten the validation in isValidOCIDirectory by
also rejecting empty manifests in index.json and verifying that the blob files
referenced by the index exist under blobs/, so interrupted mirror directories
are not reused and the failure is caught before GetOCIImageFromIndex and
ExtractOCILayers.

---

Nitpick comments:
In `@internal/pkg/release/local_stored_collector_test.go`:
- Around line 925-959: The “incomplete directory is removed and re-downloaded”
test uses a dead mirrorRun variable that never proves Mirror.Run was called.
Update this subtest around ensureReleaseInOCIFormat and MockMirror so it
actually asserts re-download occurred, either by using the mock’s
failure/success behavior consistently with the neighboring subtests or by
recording invocation on the mock and asserting it. Remove the unused mirrorRun
placeholder and keep the assertions focused on observable Mirror.Run execution
plus the recreated directory.
🪄 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: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 7c12f6c1-08a8-49c2-849b-09bdc98dc5c8

📥 Commits

Reviewing files that changed from the base of the PR and between da2bf16 and 7ac1ebb.

📒 Files selected for processing (2)
  • internal/pkg/release/local_stored_collector.go
  • internal/pkg/release/local_stored_collector_test.go

Comment on lines +227 to +266
// isValidOCIDirectory checks whether a directory contains a valid OCI image
// layout by verifying the presence and basic validity of oci-layout and
// index.json files. This is used to detect partially written directories left
// behind by interrupted mirror operations (OCPBUGS-77670).
func isValidOCIDirectory(dir string) bool {
// Check oci-layout
ociLayoutPath := filepath.Join(dir, "oci-layout")
ociLayoutData, err := os.ReadFile(ociLayoutPath)
if err != nil {
return false
}
var ociLayout struct {
ImageLayoutVersion string `json:"imageLayoutVersion"`
}
if err := json.Unmarshal(ociLayoutData, &ociLayout); err != nil {
return false
}
if ociLayout.ImageLayoutVersion == "" {
return false
}

// Check index.json
indexPath := filepath.Join(dir, "index.json")
indexData, err := os.ReadFile(indexPath)
if err != nil {
return false
}
var index struct {
SchemaVersion int `json:"schemaVersion"`
}
if err := json.Unmarshal(indexData, &index); err != nil {
return false
}
if index.SchemaVersion == 0 {
return false
}

return true
}

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## file outline\n'
ast-grep outline internal/pkg/release/local_stored_collector.go --view expanded || true

printf '\n## relevant symbols\n'
rg -n "isValidOCIDirectory|GetOCIImageFromIndex|ExtractOCILayers|ensureReleaseInOCIFormat|oci-layout|index.json|manifests|blobs/" internal/pkg/release -n

printf '\n## relevant slices\n'
sed -n '180,320p' internal/pkg/release/local_stored_collector.go
sed -n '1,260p' internal/pkg/release/*.go

Repository: openshift/oc-mirror

Length of output: 24474


Validation still misses incomplete OCI layouts isValidOCIDirectory accepts directories with only a parseable oci-layout and index.json, so an interrupted mirror can still be reused even when index.json has no manifests or the referenced blobs are missing. That defers the failure to GetOCIImageFromIndex/ExtractOCILayers instead of forcing a re-download here. Consider rejecting empty manifests and checking the referenced blob paths under blobs/.

🤖 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 `@internal/pkg/release/local_stored_collector.go` around lines 227 - 266,
isValidOCIDirectory is too permissive because it only checks that oci-layout and
index.json are parseable, so incomplete OCI layouts can still be treated as
valid. Tighten the validation in isValidOCIDirectory by also rejecting empty
manifests in index.json and verifying that the blob files referenced by the
index exist under blobs/, so interrupted mirror directories are not reused and
the failure is caught before GetOCIImageFromIndex and ExtractOCILayers.

@openshift-ci

openshift-ci Bot commented Jul 6, 2026

Copy link
Copy Markdown

@redhat-chai-bot: 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/lint 7ac1ebb link true /test lint

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.

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

Labels

jira/invalid-bug Indicates that a referenced Jira bug is invalid for the branch this PR is targeting. jira/severity-important Referenced Jira bug's severity is important for the branch this PR is targeting. 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.

2 participants