Skip to content

MCO-2468: Dump compact cache to CM for persistence - #6379

Open
pablintino wants to merge 1 commit into
openshift:mainfrom
pablintino:cm-persistance-for-cache
Open

MCO-2468: Dump compact cache to CM for persistence#6379
pablintino wants to merge 1 commit into
openshift:mainfrom
pablintino:cm-persistance-for-cache

Conversation

@pablintino

@pablintino pablintino commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

- What I did

This change dumps the cache (a reduced version of it with the bare minimal for OS Image Streams) to a new CM to allow new MCC pods that use a different name and thus, a new cache file, to read already existing cache info. It's specially useful in disconnected environments.

- How to verify it

TBD

- Description for the changelog

Dump a reduced version of the image cache to a CM to allow it to survive MCC Pod recreations.

Summary by CodeRabbit

  • New Features

    • Image inspection cache data now persists across application restarts.
    • Cache updates are synchronized automatically, including during graceful shutdown.
    • Cache warming occurs before controllers begin processing, improving startup readiness.
    • Irrelevant or outdated image-stream entries are filtered from cached data.
  • Bug Fixes

    • Improved cache eviction and synchronization behavior for more consistent image inspection results.
    • Added safeguards for oversized, invalid, or duplicate cached data.

This change dumps the cache (a reduced version of it with the bare
minimal for OS Image Streams) to a new CM to allow new MCC pods that use
a different name and thus, a new cache file, to read already existing
cache info. It's specially useful in disconnected environments.

Signed-off-by: Pablo Rodriguez Nava <git@amail.pablintino.eu>
@openshift-ci-robot

openshift-ci-robot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

@pablintino: This pull request references MCO-2468 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 "5.0.0" version, but no target version was set.

Details

In response to this:

- What I did

This change dumps the cache (a reduced version of it with the bare minimal for OS Image Streams) to a new CM to allow new MCC pods that use a different name and thus, a new cache file, to read already existing cache info. It's specially useful in disconnected environments.

- How to verify it

TBD

- Description for the changelog

Dump a reduced version of the image cache to a CM to allow it to survive MCC Pod recreations.

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 openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Aug 6, 2026
@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: LGTM mode

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Walkthrough

The change adds ConfigMap-backed inspection-cache synchronization, cache-entry filtering and transformation, mutation notifications, and coordinated controller startup. OSImageStream entries receive specialized filtering, and cache-related tests cover persistence, eviction, loading, and transformation behavior.

Changes

Inspection cache persistence

Layer / File(s) Summary
Cache synchronization contracts
pkg/imageutils/inspect_cache.go, pkg/imageutils/inspect_cache_test.go
FileInspectionCache now supports external loading, snapshots, mutation notifications, deep copies, eviction, and coordinated startup.
ConfigMap persistence and entry selection
pkg/imageutils/configmap_cache_syncer.go, pkg/imageutils/cache_entry_transformer.go, pkg/imageutils/*_test.go
ConfigMapCacheSyncer persists filtered and transformed snapshots with debounce, size, version, and update handling.
OSImageStream cache filtering
pkg/osimagestream/entry_transformer.go, pkg/osimagestream/image_data.go, pkg/osimagestream/imagestream_source.go, pkg/osimagestream/entry_transformer_test.go
OSImageStream callbacks retain relevant cache entries and matching ImageStream tags.
Controller startup integration
cmd/machine-config-controller/start.go, pkg/controller/common/constants.go, pkg/controller/pinnedimageset/cache_warmer_test.go, test/e2e-2of2/osimagestream_test.go
The controller always initializes and starts the ConfigMap-backed cache, registers cache evicters, and keeps OSImageStream setup feature-gated.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant machine-config-controller
  participant FileInspectionCache
  participant ConfigMapCacheSyncer
  participant Kubernetes ConfigMap
  machine-config-controller->>ConfigMapCacheSyncer: Create syncer
  machine-config-controller->>FileInspectionCache: Start cache
  FileInspectionCache->>ConfigMapCacheSyncer: Load persisted entries
  ConfigMapCacheSyncer->>Kubernetes ConfigMap: Read cache data
  FileInspectionCache->>ConfigMapCacheSyncer: Notify after mutations
  ConfigMapCacheSyncer->>Kubernetes ConfigMap: Debounced snapshot update
Loading
🚥 Pre-merge checks | ✅ 14 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 clearly summarizes the main change: persisting a compact image cache in a ConfigMap.
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 Changed tests use static Go Test function names; no Ginkgo title calls or runtime-derived values were found in affected packages.
Test Structure And Quality ✅ Passed The PR adds standard Go testing/Testify tests only; no Ginkgo/Gomega test code or Ginkgo lifecycle blocks are present, so this Ginkgo-specific check is not applicable.
Microshift Test Compatibility ✅ Passed No new Ginkgo e2e tests were added. The only e2e change updates an existing Go test's cache constructor call, so this MicroShift compatibility check is not applicable.
Single Node Openshift (Sno) Test Compatibility ✅ Passed No new Ginkgo e2e tests were added. The only changed e2e test is standard TestCachedInspectorFactory and only updates cache construction; added tests are unit tests.
Topology-Aware Scheduling Compatibility ✅ Passed The PR changes cache synchronization and controller startup only; no deployment manifests, pod affinity, topology spread, node selectors, tolerations, replica sizing, or PDB constraints were added.
Ote Binary Stdout Contract ✅ Passed The PR changes no OTE entrypoint or suite setup; added klog calls are runtime paths, and the controller sets logtostderr=true before startup logging.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed The PR adds no Ginkgo e2e tests. Its only e2e change passes nil to NewFileInspectionCache; new tests use Go testing, fake clients, and local fixtures without network access.
No-Weak-Crypto ✅ Passed The PR adds no weak-crypto APIs, algorithms, ECB mode, custom cryptography, or secret/token comparisons; repository MD5 uses are outside this patch.
Container-Privileges ✅ Passed The PR changes no container or Kubernetes security settings. Changed files contain no privilege markers, and the MCC deployment runs as non-root UID 65534.
No-Sensitive-Data-In-Logs ✅ Passed New logs contain generic status, byte counts, entry counts, or wrapped operation errors; no cache contents, labels, image data, credentials, PII, tokens, session IDs, or URLs are logged.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@openshift-ci

openshift-ci Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: pablintino

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 Aug 6, 2026

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

🧹 Nitpick comments (3)
pkg/imageutils/configmap_cache_syncer_test.go (1)

147-156: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

TestConfigMapCacheSyncer_SaveSkipsDuplicate does not verify deduplication.

The test calls save twice and asserts only that both calls return nil. It passes even if the second call issues a full update. Assert that the second save performs no write. Check the ResourceVersion of the ConfigMap, or inspect the actions recorded by the fake client.

🧪 Proposed fix
 func TestConfigMapCacheSyncer_SaveSkipsDuplicate(t *testing.T) {
-	syncer, _ := newFakeSyncer(t)
+	syncer, client := newFakeSyncer(t)
 
 	entries := map[string]*InspectionCacheEntry{
 		"sha256:aaa": {Labels: map[string]string{"k": "v"}},
 	}
 
 	require.NoError(t, syncer.save(context.Background(), entries))
+
+	first, err := client.CoreV1().ConfigMaps(testNamespace).Get(context.Background(), testCMName, metav1.GetOptions{})
+	require.NoError(t, err)
+
 	require.NoError(t, syncer.save(context.Background(), entries))
+
+	second, err := client.CoreV1().ConfigMaps(testNamespace).Get(context.Background(), testCMName, metav1.GetOptions{})
+	require.NoError(t, err)
+	assert.Equal(t, first.ResourceVersion, second.ResourceVersion, "second save must not write the ConfigMap")
 }
🤖 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/imageutils/configmap_cache_syncer_test.go` around lines 147 - 156, Update
TestConfigMapCacheSyncer_SaveSkipsDuplicate to verify the second save performs
no write, not merely that it succeeds. After the first save, inspect the fake
client’s recorded actions or the ConfigMap ResourceVersion, then assert it is
unchanged after the second save.
pkg/imageutils/inspect_cache.go (1)

208-216: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Start cannot detect a failed syncer start.

CacheSyncer.Start returns no value. In ConfigMapCacheSyncer.Start, a WaitForCacheSync timeout logs a warning and returns without launching the sync loop. FileInspectionCache.Start then calls loadFromSyncer, which reads an unsynced lister and receives no entries. The cache silently runs without external persistence for the whole process lifetime.

Consider returning an error from CacheSyncer.Start and propagating it, so the caller can log or retry.

🤖 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/imageutils/inspect_cache.go` around lines 208 - 216, Update the
CacheSyncer.Start contract to return an error, including the
ConfigMapCacheSyncer.Start timeout path, and propagate that result through
FileInspectionCache.Start so a failed syncer initialization is surfaced instead
of continuing silently. Update all implementations and call sites to handle the
returned error while preserving normal startup and eviction behavior.
pkg/imageutils/inspect_cache_test.go (1)

223-235: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

TestFileInspectionCache_StartSyncNoFlushWithoutChanges uses a fixed sleep.

time.Sleep(200 * time.Millisecond) adds fixed runtime to every test run and stays sensitive to scheduling on loaded CI machines. Prefer assert.Never with the same condition, which fails fast and states the intent.

🤖 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/imageutils/inspect_cache_test.go` around lines 223 - 235, Replace the
fixed time.Sleep in TestFileInspectionCache_StartSyncNoFlushWithoutChanges with
assert.Never, polling syncer.saveCount over the equivalent observation window to
verify it remains zero without changes. Keep the existing cache startup and
cancellation setup unchanged.
🤖 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 `@cmd/machine-config-controller/start.go`:
- Around line 106-108: Defer persisted-entry loading in NewFileInspectionCache
until the namespaced informer has synchronized, or update
ConfigMapCacheSyncer.Load to use a direct client read during initial
construction; ensure Start does not leave the cache empty when the ConfigMap
already exists. Add a startup test covering a pre-existing cache ConfigMap with
an initially unstarted informer.

In `@pkg/imageutils/cache_entry_transformer.go`:
- Around line 12-34: Update NewCacheFileTransformer to deep-copy the cache entry
before passing its file data to transform, ensuring the callback receives an
isolated byte slice and cannot mutate the live entry. Preserve the existing
behavior for missing files and transformation errors, and return the copied
entry with the transformed data on success.

In `@pkg/imageutils/inspect_cache_test.go`:
- Around line 159-183: Protect mockSyncer.saved and saveCount with a mutex,
locking writes in Start’s goroutine and reads through a state() accessor that
returns a consistent snapshot. Update the affected tests’ direct field
assertions and Eventually callbacks to use state() instead.

In `@pkg/imageutils/inspect_cache.go`:
- Around line 225-245: Update FileInspectionCache.loadFromSyncer to call
saveLocked after merging the loaded entries while c.mu remains held, ensuring
restored entries are persisted to the local file before returning.

---

Nitpick comments:
In `@pkg/imageutils/configmap_cache_syncer_test.go`:
- Around line 147-156: Update TestConfigMapCacheSyncer_SaveSkipsDuplicate to
verify the second save performs no write, not merely that it succeeds. After the
first save, inspect the fake client’s recorded actions or the ConfigMap
ResourceVersion, then assert it is unchanged after the second save.

In `@pkg/imageutils/inspect_cache_test.go`:
- Around line 223-235: Replace the fixed time.Sleep in
TestFileInspectionCache_StartSyncNoFlushWithoutChanges with assert.Never,
polling syncer.saveCount over the equivalent observation window to verify it
remains zero without changes. Keep the existing cache startup and cancellation
setup unchanged.

In `@pkg/imageutils/inspect_cache.go`:
- Around line 208-216: Update the CacheSyncer.Start contract to return an error,
including the ConfigMapCacheSyncer.Start timeout path, and propagate that result
through FileInspectionCache.Start so a failed syncer initialization is surfaced
instead of continuing silently. Update all implementations and call sites to
handle the returned error while preserving normal startup and eviction behavior.
🪄 Autofix

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: 6ee4b366-5900-4bed-bf1d-853b9df1406d

📥 Commits

Reviewing files that changed from the base of the PR and between 13ea39a and 491469e.

📒 Files selected for processing (14)
  • cmd/machine-config-controller/start.go
  • pkg/controller/common/constants.go
  • pkg/controller/pinnedimageset/cache_warmer_test.go
  • pkg/imageutils/cache_entry_transformer.go
  • pkg/imageutils/cache_entry_transformer_test.go
  • pkg/imageutils/configmap_cache_syncer.go
  • pkg/imageutils/configmap_cache_syncer_test.go
  • pkg/imageutils/inspect_cache.go
  • pkg/imageutils/inspect_cache_test.go
  • pkg/osimagestream/entry_transformer.go
  • pkg/osimagestream/entry_transformer_test.go
  • pkg/osimagestream/image_data.go
  • pkg/osimagestream/imagestream_source.go
  • test/e2e-2of2/osimagestream_test.go

Comment on lines +106 to +108
inspectionCache := imageutils.NewFileInspectionCache(
path.Join(startOpts.streamsCache, "image-inspection.json"), 48*time.Hour, syncer,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Load the ConfigMap only after its informer is ready.

NewFileInspectionCache loads persisted entries during construction. At this point, the namespaced informer factory has not started yet. The ConfigMap lister in ConfigMapCacheSyncer.Load therefore returns NotFound, and the cache starts empty. inspectionCache.Start waits for synchronization later, but it does not load the persisted entries again.

Defer the external load until after ConfigMap informer synchronization, or make the initial load use a direct Kubernetes client read. Add a startup test with a pre-existing cache ConfigMap and an initially unstarted informer.

🤖 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 `@cmd/machine-config-controller/start.go` around lines 106 - 108, Defer
persisted-entry loading in NewFileInspectionCache until the namespaced informer
has synchronized, or update ConfigMapCacheSyncer.Load to use a direct client
read during initial construction; ensure Start does not leave the cache empty
when the ConfigMap already exists. Add a startup test covering a pre-existing
cache ConfigMap with an initially unstarted informer.

Comment on lines +12 to +34
// NewCacheFileTransformer returns a CacheEntryTransformer that applies a
// transformation function to a cached file matching the given path. Other
// files and labels are preserved.
func NewCacheFileTransformer(path string, transform func([]byte) ([]byte, error)) CacheEntryTransformer {
return func(_ string, entry *InspectionCacheEntry) *InspectionCacheEntry {
if entry.Files == nil {
return entry
}
data, ok := entry.Files[path]
if !ok {
return entry
}

transformed, err := transform(data)
if err != nil {
return entry
}

cp := entry.DeepCopy()
cp.Files[path] = transformed
return cp
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Copy the entry before calling transform.

Line 25 passes the live entry.Files[path] slice to the callback. A callback can modify that slice before Line 30 creates the copy. This violates the no-mutation contract and can corrupt the live cache entry.

Proposed fix
-		transformed, err := transform(data)
+		cp := entry.DeepCopy()
+		transformed, err := transform(cp.Files[path])
 		if err != nil {
 			return entry
 		}
 
-		cp := entry.DeepCopy()
 		cp.Files[path] = transformed
 		return cp
📝 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
// NewCacheFileTransformer returns a CacheEntryTransformer that applies a
// transformation function to a cached file matching the given path. Other
// files and labels are preserved.
func NewCacheFileTransformer(path string, transform func([]byte) ([]byte, error)) CacheEntryTransformer {
return func(_ string, entry *InspectionCacheEntry) *InspectionCacheEntry {
if entry.Files == nil {
return entry
}
data, ok := entry.Files[path]
if !ok {
return entry
}
transformed, err := transform(data)
if err != nil {
return entry
}
cp := entry.DeepCopy()
cp.Files[path] = transformed
return cp
}
}
// NewCacheFileTransformer returns a CacheEntryTransformer that applies a
// transformation function to a cached file matching the given path. Other
// files and labels are preserved.
func NewCacheFileTransformer(path string, transform func([]byte) ([]byte, error)) CacheEntryTransformer {
return func(_ string, entry *InspectionCacheEntry) *InspectionCacheEntry {
if entry.Files == nil {
return entry
}
data, ok := entry.Files[path]
if !ok {
return entry
}
cp := entry.DeepCopy()
transformed, err := transform(cp.Files[path])
if err != nil {
return entry
}
cp.Files[path] = transformed
return cp
}
}
🤖 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/imageutils/cache_entry_transformer.go` around lines 12 - 34, Update
NewCacheFileTransformer to deep-copy the cache entry before passing its file
data to transform, ensuring the callback receives an isolated byte slice and
cannot mutate the live entry. Preserve the existing behavior for missing files
and transformation errors, and return the copied entry with the transformed data
on success.

Comment on lines +159 to +183
type mockSyncer struct {
loadEntries map[string]*InspectionCacheEntry
loadErr error
saved map[string]*InspectionCacheEntry
saveCount int
}

func (m *mockSyncer) Load(_ context.Context) (map[string]*InspectionCacheEntry, error) {
return m.loadEntries, m.loadErr
}

func (m *mockSyncer) Start(ctx context.Context, src SyncableCache, debounce time.Duration) {
ch := src.SyncNotify()
go func() {
for waitForNotify(ctx, ch) {
if !debounceDrain(ctx, ch, debounce) {
break
}
m.saved = src.Snapshot()
m.saveCount++
}
m.saved = src.Snapshot()
m.saveCount++
}()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

mockSyncer has a data race on saved and saveCount.

The goroutine started in Start writes m.saved and m.saveCount. The tests read both fields from the test goroutine, inside require.Eventually and in the final assertions. No synchronization protects these fields. go test -race will report a race in TestFileInspectionCache_StartSyncFlushesAfterPut, TestFileInspectionCache_StartSyncNoFlushWithoutChanges, TestFileInspectionCache_StartSyncFlushOnShutdown, and TestFileInspectionCache_StartSyncEvictionNotifies.

Guard the fields with a mutex and add accessor methods.

🔒 Proposed fix
 type mockSyncer struct {
+	mu          sync.Mutex
 	loadEntries map[string]*InspectionCacheEntry
 	loadErr     error
 	saved       map[string]*InspectionCacheEntry
 	saveCount   int
 }
 
+func (m *mockSyncer) record(snapshot map[string]*InspectionCacheEntry) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	m.saved = snapshot
+	m.saveCount++
+}
+
+func (m *mockSyncer) state() (map[string]*InspectionCacheEntry, int) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	return m.saved, m.saveCount
+}
+
 func (m *mockSyncer) Load(_ context.Context) (map[string]*InspectionCacheEntry, error) {
 	return m.loadEntries, m.loadErr
 }
 
 func (m *mockSyncer) Start(ctx context.Context, src SyncableCache, debounce time.Duration) {
 	ch := src.SyncNotify()
 	go func() {
 		for waitForNotify(ctx, ch) {
 			if !debounceDrain(ctx, ch, debounce) {
 				break
 			}
-			m.saved = src.Snapshot()
-			m.saveCount++
+			m.record(src.Snapshot())
 		}
-		m.saved = src.Snapshot()
-		m.saveCount++
+		m.record(src.Snapshot())
 	}()
 }

Then update the assertions to use state(), for example:

require.Eventually(t, func() bool {
	_, count := syncer.state()
	return count > 0
}, 5*time.Second, 50*time.Millisecond)

saved, _ := syncer.state()
assert.Contains(t, saved, "sha256:new")
🤖 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/imageutils/inspect_cache_test.go` around lines 159 - 183, Protect
mockSyncer.saved and saveCount with a mutex, locking writes in Start’s goroutine
and reads through a state() accessor that returns a consistent snapshot. Update
the affected tests’ direct field assertions and Eventually callbacks to use
state() instead.

Comment on lines +225 to +245
func (c *FileInspectionCache) loadFromSyncer(ctx context.Context) {
entries, err := c.syncer.Load(ctx)
if err != nil {
klog.Warningf("Failed to load inspection cache from external store: %v", err)
return
}
if len(entries) == 0 {
return
}

c.mu.Lock()
defer c.mu.Unlock()
for digest, entry := range entries {
existing, ok := c.entries[digest]
// Keep the local entry if it is the same age or newer.
if ok && !existing.CreatedAt.Before(entry.CreatedAt) {
continue
}
c.entries[digest] = entry.DeepCopy()
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Entries loaded from the external store are not written to the local file.

loadFromSyncer mutates c.entries but does not call saveLocked or notifySync. If the pod restarts before the next Put or eviction, the restored entries are lost from the on-disk cache. Add a saveLocked call after the merge loop.

💾 Proposed fix
 	c.mu.Lock()
 	defer c.mu.Unlock()
+	loaded := false
 	for digest, entry := range entries {
 		existing, ok := c.entries[digest]
 		// Keep the local entry if it is the same age or newer.
 		if ok && !existing.CreatedAt.Before(entry.CreatedAt) {
 			continue
 		}
 		c.entries[digest] = entry.DeepCopy()
+		loaded = true
+	}
+	if loaded {
+		if err := c.saveLocked(); err != nil {
+			klog.Warningf("Failed to persist inspection cache after external load: %v", err)
+		}
 	}
 }
📝 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
func (c *FileInspectionCache) loadFromSyncer(ctx context.Context) {
entries, err := c.syncer.Load(ctx)
if err != nil {
klog.Warningf("Failed to load inspection cache from external store: %v", err)
return
}
if len(entries) == 0 {
return
}
c.mu.Lock()
defer c.mu.Unlock()
for digest, entry := range entries {
existing, ok := c.entries[digest]
// Keep the local entry if it is the same age or newer.
if ok && !existing.CreatedAt.Before(entry.CreatedAt) {
continue
}
c.entries[digest] = entry.DeepCopy()
}
}
func (c *FileInspectionCache) loadFromSyncer(ctx context.Context) {
entries, err := c.syncer.Load(ctx)
if err != nil {
klog.Warningf("Failed to load inspection cache from external store: %v", err)
return
}
if len(entries) == 0 {
return
}
c.mu.Lock()
defer c.mu.Unlock()
loaded := false
for digest, entry := range entries {
existing, ok := c.entries[digest]
// Keep the local entry if it is the same age or newer.
if ok && !existing.CreatedAt.Before(entry.CreatedAt) {
continue
}
c.entries[digest] = entry.DeepCopy()
loaded = true
}
if loaded {
if err := c.saveLocked(); err != nil {
klog.Warningf("Failed to persist inspection cache after external load: %v", err)
}
}
}
🤖 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/imageutils/inspect_cache.go` around lines 225 - 245, Update
FileInspectionCache.loadFromSyncer to call saveLocked after merging the loaded
entries while c.mu remains held, ensuring restored entries are persisted to the
local file before returning.

@pablintino

Copy link
Copy Markdown
Contributor Author

/hold Still needs some work to get it working

@openshift-ci openshift-ci Bot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Aug 6, 2026
@openshift-ci

openshift-ci Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

@pablintino: The following tests 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/verify 491469e link true /test verify
ci/prow/perfscale-control-plane-6nodes 491469e link false /test perfscale-control-plane-6nodes

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

approved Indicates a PR has been approved by an approver from all required OWNERS files. do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. 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