Skip to content

Commit 65776df

Browse files
authored
Merge pull request #1606 from buildpacks/jjbustamante/fix/issue-1590-restorer-mirror-selection
Fix restorer to use selected mirror for Platform API 0.14
2 parents 41468ad + 14bd5aa commit 65776df

3 files changed

Lines changed: 149 additions & 5 deletions

File tree

cmd/lifecycle/restorer.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -214,11 +214,9 @@ func (r *restoreCmd) runImageAccessCheck(runImageName string) (string, error) {
214214
return "", err
215215
}
216216

217-
if !runToml.Contains(runImageName) {
218-
return runImageName, nil
219-
}
220-
221-
return platform.BestRunImageMirrorFor("", runToml.FindByRef(runImageName), r.AccessChecker())
217+
// For Platform API 0.14+, use the new function that validates the already-selected
218+
// run image from analyzed.toml without retrying the primary image first (issue #1590)
219+
return platform.ResolveRunImageFromAnalyzed(runImageName, runToml, r.AccessChecker())
222220
}
223221

224222
func (r *restoreCmd) needsUpdating(runImage *files.RunImage, group buildpack.Group) bool {

platform/run_image.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,33 @@ func GetRunImageFromMetadata(inputs LifecycleInputs, md files.LayersMetadata) (f
126126
return files.RunImageForExport{}, errors.New("no run image metadata available")
127127
}
128128
}
129+
130+
// ResolveRunImageFromAnalyzed resolves the run image for Platform API 0.14+ restorer.
131+
// It takes the already-selected run image from analyzed.toml and validates it's accessible.
132+
// For issue #1590: This should NOT retry with the primary image if a mirror was already selected.
133+
func ResolveRunImageFromAnalyzed(runImageFromAnalyzed string, runToml files.Run, checkReadAccess CheckReadAccess) (string, error) {
134+
// If the run image is not in run.toml, return it as-is
135+
// (e.g., extensions switched the run image)
136+
if !runToml.Contains(runImageFromAnalyzed) {
137+
return runImageFromAnalyzed, nil
138+
}
139+
140+
// FIX for issue #1590:
141+
// The analyzer already selected an accessible run image and wrote it to analyzed.toml.
142+
// For Platform API 0.14, the restorer should just validate that THIS specific image
143+
// is still accessible, not redo the entire selection process.
144+
//
145+
// We must create a keychain for the selected image to check access
146+
keychain, err := auth.DefaultKeychain(runImageFromAnalyzed)
147+
if err != nil {
148+
return "", fmt.Errorf("unable to create keychain: %w", err)
149+
}
150+
151+
// Check if the already-selected run image is accessible
152+
if ok, _ := checkReadAccess(runImageFromAnalyzed, keychain); ok {
153+
return runImageFromAnalyzed, nil
154+
}
155+
156+
// If the selected image is not accessible, return an error with the image name
157+
return "", fmt.Errorf("selected run image '%s' is not accessible", runImageFromAnalyzed)
158+
}

platform/run_image_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package platform_test
22

33
import (
4+
"fmt"
45
"path/filepath"
56
"testing"
67

@@ -277,4 +278,119 @@ func testRunImage(t *testing.T, when spec.G, it spec.S) {
277278
})
278279
})
279280
})
281+
282+
when(".ResolveRunImageFromAnalyzed", func() {
283+
when("Platform API 0.14+", func() {
284+
it("should use already-selected mirror from analyzed.toml", func() {
285+
// Reproduces issue #1590
286+
// The restorer should validate the already-selected mirror from analyzed.toml
287+
// without retrying the primary image first
288+
//
289+
// The scenario:
290+
// 1. Analyzer already selected "localhost:5000/pack-test/run" (accessible mirror)
291+
// 2. Wrote it to analyzed.toml as run-image.image
292+
// 3. Restorer reads this value: runImageName = "localhost:5000/pack-test/run"
293+
// 4. Restorer has run.toml with primary + mirrors
294+
// 5. Restorer should validate the already-selected mirror
295+
296+
// STEP 1: The run image name from analyzed.toml (already selected by analyzer)
297+
runImageNameFromAnalyzed := "localhost:5000/pack-test/run"
298+
299+
// STEP 2: The run.toml with full entry (primary + mirrors)
300+
runToml := files.Run{
301+
Images: []files.RunImageForExport{
302+
{
303+
Image: "pack-test/run", // Primary - INACCESSIBLE
304+
Mirrors: []string{"localhost:5000/pack-test/run"}, // Mirror - accessible
305+
},
306+
},
307+
}
308+
309+
// STEP 3: Access checker - simulates the real scenario
310+
callCount := make(map[string]int)
311+
checkAccess := func(image string, _ authn.Keychain) (bool, error) {
312+
callCount[image]++
313+
if image == "pack-test/run" {
314+
// Primary is NOT accessible (401 error)
315+
return false, fmt.Errorf("failed to get remote image: unauthorized")
316+
}
317+
if image == "localhost:5000/pack-test/run" {
318+
// Mirror IS accessible
319+
return true, nil
320+
}
321+
return false, fmt.Errorf("unknown image: %s", image)
322+
}
323+
324+
// STEP 4: Call the function (simulates what restorer does)
325+
result, err := platform.ResolveRunImageFromAnalyzed(runImageNameFromAnalyzed, runToml, checkAccess)
326+
327+
// EXPECTATIONS:
328+
// 1. Should succeed
329+
h.AssertNil(t, err)
330+
h.AssertEq(t, result, "localhost:5000/pack-test/run")
331+
332+
// 2. CRITICAL: Should NOT have checked the primary image!
333+
// Since we already know the mirror was selected, we shouldn't retry primary
334+
if callCount["pack-test/run"] > 0 {
335+
t.Fatalf("FAIL: Should not check primary 'pack-test/run' when mirror was already selected. Call count: %v", callCount)
336+
}
337+
338+
// 3. Should have checked the selected mirror
339+
h.AssertEq(t, callCount["localhost:5000/pack-test/run"], 1)
340+
})
341+
342+
it("returns the run image as-is when not in run.toml", func() {
343+
// If the run image from analyzed.toml is not found in run.toml
344+
// (e.g., extensions switched the run image), just return it
345+
runImageNameFromAnalyzed := "some-extension-switched-image:latest"
346+
347+
runToml := files.Run{
348+
Images: []files.RunImageForExport{
349+
{
350+
Image: "pack-test/run",
351+
Mirrors: []string{"localhost:5000/pack-test/run"},
352+
},
353+
},
354+
}
355+
356+
checkAccess := func(_ string, _ authn.Keychain) (bool, error) {
357+
return true, nil
358+
}
359+
360+
result, err := platform.ResolveRunImageFromAnalyzed(runImageNameFromAnalyzed, runToml, checkAccess)
361+
362+
h.AssertNil(t, err)
363+
h.AssertEq(t, result, "some-extension-switched-image:latest")
364+
})
365+
366+
it("fails with helpful message when selected mirror is not accessible", func() {
367+
// If the environment changed between analyzer and restorer phases,
368+
// and the previously-selected mirror is no longer accessible,
369+
// we should fail with a clear error message that includes the image name
370+
runImageNameFromAnalyzed := "localhost:5000/pack-test/run"
371+
372+
runToml := files.Run{
373+
Images: []files.RunImageForExport{
374+
{
375+
Image: "pack-test/run",
376+
Mirrors: []string{"localhost:5000/pack-test/run"},
377+
},
378+
},
379+
}
380+
381+
checkAccess := func(_ string, _ authn.Keychain) (bool, error) {
382+
// Mirror is no longer accessible
383+
return false, fmt.Errorf("failed to get remote image: connection refused")
384+
}
385+
386+
_, err := platform.ResolveRunImageFromAnalyzed(runImageNameFromAnalyzed, runToml, checkAccess)
387+
388+
// Should fail with clear message including the image name
389+
h.AssertNotNil(t, err)
390+
h.AssertStringContains(t, err.Error(), "selected run image")
391+
h.AssertStringContains(t, err.Error(), "localhost:5000/pack-test/run")
392+
h.AssertStringContains(t, err.Error(), "not accessible")
393+
})
394+
})
395+
})
280396
}

0 commit comments

Comments
 (0)