Skip to content
12 changes: 7 additions & 5 deletions cmd/lifecycle/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,11 +344,13 @@ func (e *exportCmd) initRemoteAppImage(analyzedMD files.Analyzed) (imgutil.Image
appOpts = append(appOpts, remote.WithCreatedAt(e.customSourceDateEpoch()))
}

appImage, err := remote.NewImage(
e.OutputImageRef,
e.keychain,
appOpts...,
)
appImage, err := phase.OpenRemoteImage(cmd.DefaultLogger, func() (imgutil.Image, error) {
return remote.NewImage(
e.OutputImageRef,
e.keychain,
appOpts...,
)
})
if err != nil {
return nil, "", cmd.FailErr(err, "create new app image")
}
Expand Down
16 changes: 8 additions & 8 deletions cmd/lifecycle/rebaser.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,14 @@ func (r *rebaseCmd) Exec() error {
local.FromBaseImage(r.RunImageRef),
)
} else {
var opts []imgutil.ImageOption
opts = append(opts, append(image.GetInsecureOptions(r.InsecureRegistries), remote.FromBaseImage(r.RunImageRef))...)

newBaseImage, err = remote.NewImage(
r.RunImageRef,
r.keychain,
opts...,
)
newBaseImage, err = phase.OpenRemoteImage(cmd.DefaultLogger, func() (imgutil.Image, error) {
opts := append(image.GetInsecureOptions(r.InsecureRegistries), remote.FromBaseImage(r.RunImageRef))
return remote.NewImage(
r.RunImageRef,
r.keychain,
opts...,
)
})
}
if err != nil || !newBaseImage.Found() {
return cmd.FailErr(err, "access run image")
Expand Down
1 change: 1 addition & 0 deletions phase/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ type LauncherConfig struct {
Metadata files.LauncherMetadata
}

// ExportOptions is the set of options for exporting an image.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@jabrown85, linting fail without comment for ExportOptions and Rebaser. Should I keep it?

type ExportOptions struct {
// WorkingImage is the image to save.
WorkingImage imgutil.Image
Expand Down
1 change: 1 addition & 0 deletions phase/rebaser.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var (
msgUnableToSatisfyTargetConstraints = "unable to satisfy target os/arch constraints; new run image: %s, old run image: %s"
)

// Rebaser changes the underlying base image for an application image.
type Rebaser struct {
Logger log.Logger
PlatformAPI *api.Version
Expand Down
65 changes: 65 additions & 0 deletions phase/retry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package phase

import (
stderrors "errors"
"net/http"
"time"

"github.com/buildpacks/imgutil"
"github.com/google/go-containerregistry/pkg/v1/remote/transport"

"github.com/buildpacks/lifecycle/log"
)

// topLayerDelays is hardcoded array of delays
var topLayerDelays = []time.Duration{
100 * time.Millisecond,
200 * time.Millisecond,
500 * time.Millisecond,
1 * time.Second,
2 * time.Second,
}

// topLayerSleep is the function used for sleeping between retries.
// It can be replaced for testing.
var topLayerSleep = time.Sleep

// isRetryable returns true if the error is likely transient and should be retried.
// 401 Unauthorized and 403 Forbidden are not retryable as they indicate auth/config issues.
func isRetryable(err error) bool {
if tErr, ok := stderrors.AsType[*transport.Error](err); ok {
return tErr.StatusCode != http.StatusBadRequest &&
tErr.StatusCode != http.StatusUnauthorized &&
tErr.StatusCode != http.StatusForbidden &&
tErr.StatusCode != http.StatusMethodNotAllowed &&
tErr.StatusCode != http.StatusTooManyRequests
}
return true
}

// OpenRemoteImage opens a remote image with retry logic for registry mirror transient errors.
// go-containerregistry caches manifests, so each retry attempt creates a fresh image.
// Non-retryable errors (401, 403) are returned immediately without retry.
func OpenRemoteImage(logger log.Logger, newImage func() (imgutil.Image, error)) (imgutil.Image, error) {
var lastErr error
for attempt := 0; attempt <= len(topLayerDelays); attempt++ {
img, err := newImage()
if err == nil {
if _, err = img.TopLayer(); err == nil {
if attempt > 0 {
logger.Infof("Successfully opened remote image after %d retries", attempt)
}
return img, nil
}
}
lastErr = err
if !isRetryable(err) {
return nil, err
}
if attempt < len(topLayerDelays) {
logger.Warnf("Failed to open remote image (attempt %d/%d): %v, retrying in %v", attempt+1, len(topLayerDelays)+1, err, topLayerDelays[attempt])
topLayerSleep(topLayerDelays[attempt])
}
}
return nil, lastErr
}
Loading
Loading