From f23eaab324ce28647f9009b0182da52dd6a74832 Mon Sep 17 00:00:00 2001 From: chruffins <23645059+chruffins@users.noreply.github.com> Date: Wed, 12 Aug 2026 20:32:15 +0000 Subject: [PATCH 1/3] Borrow credentials for image pulls --- cmd/api/api/images.go | 5 +- cmd/api/api/images_test.go | 46 +++ cmd/api/api/pushes.go | 11 +- lib/images/credentials_test.go | 119 ++++++ lib/images/errors.go | 3 +- lib/images/manager.go | 51 ++- lib/images/oci.go | 70 +++- lib/images/storage.go | 29 +- lib/images/types.go | 4 + lib/oapi/oapi.go | 717 ++++++++++++++++----------------- openapi.yaml | 7 +- 11 files changed, 647 insertions(+), 415 deletions(-) create mode 100644 lib/images/credentials_test.go diff --git a/cmd/api/api/images.go b/cmd/api/api/images.go index 08f97e1a..e8d1c050 100644 --- a/cmd/api/api/images.go +++ b/cmd/api/api/images.go @@ -37,8 +37,9 @@ func (s *ApiService) CreateImage(ctx context.Context, request oapi.CreateImageRe log := logger.FromContext(ctx) domainReq := images.CreateImageRequest{ - Name: request.Body.Name, - Tags: toMapTags(request.Body.Tags), + Name: request.Body.Name, + Tags: toMapTags(request.Body.Tags), + Credentials: registryCredentialsToAuthn(request.Body.Credentials), } if request.Body.Platform != nil { domainReq.Platform = *request.Body.Platform diff --git a/cmd/api/api/images_test.go b/cmd/api/api/images_test.go index 2a5a8db3..3b785f27 100644 --- a/cmd/api/api/images_test.go +++ b/cmd/api/api/images_test.go @@ -27,6 +27,52 @@ func (m createImageErrManager) CreateImage(context.Context, images.CreateImageRe return nil, m.err } +type captureCreateImageManager struct { + images.Manager + req images.CreateImageRequest +} + +func (m *captureCreateImageManager) CreateImage(_ context.Context, req images.CreateImageRequest) (*images.Image, error) { + m.req = req + return &images.Image{Name: req.Name, Digest: "sha256:test", Status: images.StatusPending, CreatedAt: time.Now()}, nil +} + +func TestCreateImage_MapsBorrowedCredentials(t *testing.T) { + t.Parallel() + + username, password, token := "borrower", "secret", "registry-token" + manager := &captureCreateImageManager{} + svc := &ApiService{ImageManager: manager} + + resp, err := svc.CreateImage(context.Background(), oapi.CreateImageRequestObject{Body: &oapi.CreateImageRequest{ + Name: "registry.example/private/image:latest", + Credentials: &oapi.PushCredentials{ + Username: &username, + Password: &password, + RegistryToken: &token, + }, + }}) + require.NoError(t, err) + require.IsType(t, oapi.CreateImage202JSONResponse{}, resp) + require.NotNil(t, manager.req.Credentials) + assert.Equal(t, username, manager.req.Credentials.Username) + assert.Equal(t, password, manager.req.Credentials.Password) + assert.Equal(t, token, manager.req.Credentials.RegistryToken) +} + +func TestCreateImage_EmptyCredentialsUseServerKeychain(t *testing.T) { + t.Parallel() + + manager := &captureCreateImageManager{} + svc := &ApiService{ImageManager: manager} + _, err := svc.CreateImage(context.Background(), oapi.CreateImageRequestObject{Body: &oapi.CreateImageRequest{ + Name: "docker.io/library/alpine:latest", + Credentials: &oapi.PushCredentials{}, + }}) + require.NoError(t, err) + assert.Nil(t, manager.req.Credentials) +} + func TestCreateImage_ErrorStatusMapping(t *testing.T) { t.Parallel() diff --git a/cmd/api/api/pushes.go b/cmd/api/api/pushes.go index f409611e..8614ee36 100644 --- a/cmd/api/api/pushes.go +++ b/cmd/api/api/pushes.go @@ -26,7 +26,7 @@ func (s *ApiService) CreatePush(ctx context.Context, request oapi.CreatePushRequ domainReq := imagepush.PushRequest{ Image: request.Body.Image, Target: request.Body.Target, - Credentials: pushCredentialsToAuthn(request.Body.Credentials), + Credentials: registryCredentialsToAuthn(request.Body.Credentials), } if request.Body.Insecure != nil { domainReq.Insecure = *request.Body.Insecure @@ -123,11 +123,10 @@ func (s *ApiService) ListPushes(ctx context.Context, request oapi.ListPushesRequ return oapi.ListPushes200JSONResponse(out), nil } -// pushCredentialsToAuthn maps API credentials to the go-containerregistry -// auth config. Returns nil when absent or empty so the push falls back to -// the server's default credential resolution — an empty credentials object -// must not mask the keychain. -func pushCredentialsToAuthn(creds *oapi.PushCredentials) *authn.AuthConfig { +// registryCredentialsToAuthn maps borrowed API credentials to the +// go-containerregistry auth config. Nil and empty credentials preserve the +// server's default keychain behavior. +func registryCredentialsToAuthn(creds *oapi.PushCredentials) *authn.AuthConfig { if creds == nil { return nil } diff --git a/lib/images/credentials_test.go b/lib/images/credentials_test.go new file mode 100644 index 00000000..8f61282b --- /dev/null +++ b/lib/images/credentials_test.go @@ -0,0 +1,119 @@ +package images + +import ( + "context" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/kernel/hypeman/lib/paths" + "github.com/kernel/hypeman/lib/queue" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBorrowedCredentialsAuthenticateResolveAndPull(t *testing.T) { + const username = "borrower" + const password = "pull-secret" + + handler := registry.New() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotUser, gotPassword, ok := r.BasicAuth() + if !ok || gotUser != username || gotPassword != password { + w.Header().Set("WWW-Authenticate", `Basic realm="registry"`) + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + handler.ServeHTTP(w, r) + })) + defer server.Close() + + registryHost := strings.TrimPrefix(server.URL, "http://") + ref, err := name.ParseReference(registryHost+"/private/image:latest", name.Insecure) + require.NoError(t, err) + credentials := &authn.AuthConfig{Username: username, Password: password} + + img, err := random.Image(256, 1) + require.NoError(t, err) + require.NoError(t, remote.Write(ref, img, remote.WithAuth(authn.FromConfig(*credentials)))) + + client, err := newOCIClient(t.TempDir()) + require.NoError(t, err) + platform := vmPlatform() + digest, err := client.inspectManifestWithPlatformAuth(context.Background(), ref.String(), platform, credentials) + require.NoError(t, err) + + err = client.pullToOCILayoutWithPlatformAuth(context.Background(), ref.Context().Digest(digest).String(), digestToLayoutTag(digest), platform, credentials) + require.NoError(t, err) + assert.True(t, client.existsInLayout(digestToLayoutTag(digest))) +} + +func TestCreateImageRequestCredentialsAreNotPersisted(t *testing.T) { + const secret = "must-not-reach-disk" + const repository = "registry.example/private/image" + const digest = "abc" + p := paths.New(t.TempDir()) + meta := imageMetadata{ + Name: repository + ":latest", + Digest: "sha256:" + digest, + Status: StatusPending, + BorrowedAuth: true, + Request: &CreateImageRequest{ + Name: repository + ":latest", + Credentials: &authn.AuthConfig{Username: "borrower", Password: secret}, + }, + } + + require.NoError(t, writeMetadata(p, repository, digest, &meta)) + data, err := os.ReadFile(metadataPath(p, repository, digest)) + require.NoError(t, err) + assert.NotContains(t, string(data), secret) + assert.NotContains(t, string(data), "borrower") + assert.Contains(t, string(data), `"borrowed_auth": true`) +} + +func TestRecoverInterruptedCredentialedPullFailsForFreshRetry(t *testing.T) { + p := paths.New(t.TempDir()) + client, err := newOCIClient(p.SystemOCICache()) + require.NoError(t, err) + m := &manager{ + paths: p, + ociClient: client, + queue: queue.New(1), + readySubscribers: make(map[string][]chan StatusEvent), + } + + const repository = "registry.example/private/image" + const digest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + meta := &imageMetadata{ + Name: repository + ":latest", + Digest: digest, + Status: StatusPulling, + BorrowedAuth: true, + Request: &CreateImageRequest{Name: repository + ":latest"}, + CreatedAt: time.Now(), + } + require.NoError(t, writeMetadata(p, repository, strings.TrimPrefix(digest, "sha256:"), meta)) + + m.RecoverInterruptedBuilds() + + stored, err := readMetadata(p, repository, strings.TrimPrefix(digest, "sha256:")) + require.NoError(t, err) + require.NotNil(t, stored.Error) + assert.Equal(t, StatusFailed, stored.Status) + assert.Equal(t, ErrBorrowedCredentialsExpired.Error(), *stored.Error) + assert.Zero(t, m.queue.QueueLength()) + + data, err := os.ReadFile(filepath.Join(p.ImageDigestDir(repository, strings.TrimPrefix(digest, "sha256:")), "metadata.json")) + require.NoError(t, err) + assert.NotContains(t, string(data), "password") +} diff --git a/lib/images/errors.go b/lib/images/errors.go index 051e5c16..22c56408 100644 --- a/lib/images/errors.go +++ b/lib/images/errors.go @@ -25,7 +25,8 @@ var ( // ErrRateLimited means the registry rejected the request with a rate-limit // response (e.g. Docker Hub's unauthenticated pull limit). Transient and // caller-actionable (retry later or authenticate), not a server fault. - ErrRateLimited = errors.New("registry rate limit exceeded") + ErrRateLimited = errors.New("registry rate limit exceeded") + ErrBorrowedCredentialsExpired = errors.New("credentialed image pull was interrupted; submit POST /images again with fresh credentials") ) // ClassifyRegistryError classifies a raw registry/go-containerregistry error into a diff --git a/lib/images/manager.go b/lib/images/manager.go index e70146ce..c0652dcb 100644 --- a/lib/images/manager.go +++ b/lib/images/manager.go @@ -12,6 +12,7 @@ import ( "sync" "time" + "github.com/google/go-containerregistry/pkg/authn" "github.com/kernel/hypeman/lib/paths" "github.com/kernel/hypeman/lib/queue" "github.com/kernel/hypeman/lib/tags" @@ -62,6 +63,7 @@ type manager struct { readyImageBytes int64 ociCacheBytes int64 metrics *Metrics + inflightAuth map[string][32]byte // keyed by digest readySubscribers map[string][]chan StatusEvent // keyed by digestHex subscriberMu sync.RWMutex } @@ -96,6 +98,14 @@ func NewManager(p *paths.Paths, maxConcurrentBuilds int, meter metric.Meter) (Ma return m, nil } +func cloneCredentials(credentials *authn.AuthConfig) *authn.AuthConfig { + if credentials == nil || (credentials.Username == "" && credentials.Password == "" && credentials.RegistryToken == "") { + return nil + } + cloned := *credentials + return &cloned +} + func (m *manager) ListImages(ctx context.Context) ([]Image, error) { metas, err := listAllMetadata(m.paths) if err != nil { @@ -111,6 +121,7 @@ func (m *manager) ListImages(ctx context.Context) ([]Image, error) { } func (m *manager) CreateImage(ctx context.Context, req CreateImageRequest) (*Image, error) { + req.Credentials = cloneCredentials(req.Credentials) if err := tags.Validate(req.Tags); err != nil { return nil, err } @@ -133,6 +144,11 @@ func (m *manager) CreateImage(ctx context.Context, req CreateImageRequest) (*Ima resolveCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() + var inspector ManifestInspector = m.ociClient + if req.Credentials != nil { + inspector = &credentialedManifestInspector{client: m.ociClient, credentials: req.Credentials} + } + var ref *ResolvedRef if normalized.IsDigest() { // A digest pin must resolve to its exact manifest (verifying the digest @@ -143,7 +159,7 @@ func (m *manager) CreateImage(ctx context.Context, req CreateImageRequest) (*Ima // resolve goes through the ManifestInspector seam, symmetric with the tag // path below, so it stays fake-testable. var actual Platform - actual, ref, err = normalized.ResolveDigest(resolveCtx, m.ociClient, platform.ToGCR()) + actual, ref, err = normalized.ResolveDigest(resolveCtx, inspector, platform.ToGCR()) if err != nil { return nil, fmt.Errorf("resolve digest manifest: %w", err) } @@ -153,7 +169,7 @@ func (m *manager) CreateImage(ctx context.Context, req CreateImageRequest) (*Ima } else { // inspectManifestWithPlatform already classifies registry errors via // ClassifyRegistryError, so just propagate with %w to keep the errors.Is chain. - ref, err = normalized.ResolveForPlatform(resolveCtx, m.ociClient, platform.ToGCR()) + ref, err = normalized.ResolveForPlatform(resolveCtx, inspector, platform.ToGCR()) if err != nil { return nil, fmt.Errorf("resolve manifest for platform %s: %w", platform, err) } @@ -251,13 +267,14 @@ func (m *manager) createAndQueueImage(ref *ResolvedRef, req CreateImageRequest, // buildImage overwrites it with the authoritative manifest platform once the // image config is pulled. meta := &imageMetadata{ - Name: ref.String(), - Digest: ref.Digest(), - Platform: requestedPlatform.String(), - Status: StatusPending, - Request: &storedReq, - Tags: tags.Clone(req.Tags), - CreatedAt: time.Now(), + Name: ref.String(), + Digest: ref.Digest(), + Platform: requestedPlatform.String(), + Status: StatusPending, + Request: &storedReq, + BorrowedAuth: req.Credentials != nil, + Tags: tags.Clone(req.Tags), + CreatedAt: time.Now(), } // Write initial metadata @@ -267,7 +284,7 @@ func (m *manager) createAndQueueImage(ref *ResolvedRef, req CreateImageRequest, // Enqueue the build using digest as the queue key for deduplication queuePos := m.queue.Enqueue(ref.Digest(), func() { - m.buildImage(context.Background(), ref) + m.buildImage(context.Background(), ref, req.Credentials) }, nil) img := meta.toImage() @@ -277,7 +294,7 @@ func (m *manager) createAndQueueImage(ref *ResolvedRef, req CreateImageRequest, return img, nil } -func (m *manager) buildImage(ctx context.Context, ref *ResolvedRef) { +func (m *manager) buildImage(ctx context.Context, ref *ResolvedRef, credentials *authn.AuthConfig) { buildStart := time.Now() buildStatus := "failed" buildDir := m.paths.SystemBuild(ref.String()) @@ -306,7 +323,7 @@ func (m *manager) buildImage(ctx context.Context, ref *ResolvedRef) { // still pulls the architecture its digest identifies. Uses the cache if the // digest is already pulled. pullRef := ref.DigestRef() - result, err := m.ociClient.pullAndExport(ctx, pullRef, ref.Digest(), tempDir) + result, err := m.ociClient.pullAndExportWithAuth(ctx, pullRef, ref.Digest(), tempDir, credentials) m.recordPullResultMetrics(ctx, ref.Digest(), result) if err != nil { m.updateStatusByDigest(ref, StatusFailed, fmt.Errorf("pull and export: %w", err)) @@ -476,6 +493,14 @@ func (m *manager) RecoverInterruptedBuilds() { for _, meta := range metas { switch meta.Status { case StatusPending, StatusPulling, StatusConverting: + if meta.BorrowedAuth { + normalized, parseErr := ParseNormalizedRef(meta.Name) + if parseErr == nil { + ref := NewResolvedRef(normalized, meta.Digest) + m.updateStatusByDigest(ref, StatusFailed, ErrBorrowedCredentialsExpired) + } + continue + } if meta.Request != nil && meta.Digest != "" { metaCopy := meta normalized, err := ParseNormalizedRef(metaCopy.Name) @@ -485,7 +510,7 @@ func (m *manager) RecoverInterruptedBuilds() { // Create a ResolvedRef since we already have the digest from metadata ref := NewResolvedRef(normalized, metaCopy.Digest) m.queue.Enqueue(metaCopy.Digest, func() { - m.buildImage(context.Background(), ref) + m.buildImage(context.Background(), ref, nil) }, nil) } } diff --git a/lib/images/oci.go b/lib/images/oci.go index 170ac8cd..33206d13 100644 --- a/lib/images/oci.go +++ b/lib/images/oci.go @@ -27,6 +27,19 @@ type ociClient struct { cacheDir string } +type credentialedManifestInspector struct { + client *ociClient + credentials *authn.AuthConfig +} + +func (i *credentialedManifestInspector) inspectManifestWithPlatform(ctx context.Context, imageRef string, platform gcr.Platform) (string, error) { + return i.client.inspectManifestWithPlatformAuth(ctx, imageRef, platform, i.credentials) +} + +func (i *credentialedManifestInspector) inspectDigestPlatform(ctx context.Context, imageRef string, requested gcr.Platform) (Platform, string, error) { + return i.client.inspectDigestPlatformAuth(ctx, imageRef, requested, i.credentials) +} + // digestToLayoutTag converts a digest to a valid OCI layout tag. // Uses just the hex portion without the algorithm prefix. // Example: "sha256:abc123..." -> "abc123..." @@ -81,6 +94,10 @@ func (c *ociClient) inspectManifest(ctx context.Context, imageRef string) (strin // inspectManifestWithPlatform synchronously inspects a remote image to get its digest // for a specific platform. func (c *ociClient) inspectManifestWithPlatform(ctx context.Context, imageRef string, platform gcr.Platform) (string, error) { + return c.inspectManifestWithPlatformAuth(ctx, imageRef, platform, nil) +} + +func (c *ociClient) inspectManifestWithPlatformAuth(ctx context.Context, imageRef string, platform gcr.Platform, credentials *authn.AuthConfig) (string, error) { ref, err := name.ParseReference(imageRef) if err != nil { return "", fmt.Errorf("parse image reference: %w", err) @@ -93,10 +110,9 @@ func (c *ociClient) inspectManifestWithPlatform(ctx context.Context, imageRef st // // Registry-error classification (rate-limit, not-found, platform-not-available) // is centralized here so callers can `%w` the result without re-wrapping. - img, err := remote.Image(ref, - remote.WithContext(ctx), - remote.WithAuthFromKeychain(authn.DefaultKeychain), - remote.WithPlatform(platform)) + opts := registryRemoteOptions(ctx, credentials) + opts = append(opts, remote.WithPlatform(platform)) + img, err := remote.Image(ref, opts...) if err != nil { return "", fmt.Errorf("fetch manifest: %w", ClassifyRegistryError(err)) } @@ -129,14 +145,16 @@ func (c *ociClient) inspectManifestWithPlatform(ctx context.Context, imageRef st // A digest absent from the registry surfaces as a not-found error, classified by // ClassifyRegistryError. func (c *ociClient) inspectDigestPlatform(ctx context.Context, imageRef string, requested gcr.Platform) (Platform, string, error) { + return c.inspectDigestPlatformAuth(ctx, imageRef, requested, nil) +} + +func (c *ociClient) inspectDigestPlatformAuth(ctx context.Context, imageRef string, requested gcr.Platform, credentials *authn.AuthConfig) (Platform, string, error) { ref, err := name.ParseReference(imageRef) if err != nil { return Platform{}, "", fmt.Errorf("parse image reference: %w", err) } - desc, err := remote.Get(ref, - remote.WithContext(ctx), - remote.WithAuthFromKeychain(authn.DefaultKeychain)) + desc, err := remote.Get(ref, registryRemoteOptions(ctx, credentials)...) if err != nil { return Platform{}, "", fmt.Errorf("fetch manifest: %w", ClassifyRegistryError(err)) } @@ -145,10 +163,9 @@ func (c *ociClient) inspectDigestPlatform(ctx context.Context, imageRef string, if desc.MediaType.IsIndex() { // Index: select the child for the requested platform. WithPlatform on the // re-parsed digest ref is correct here because the digest names the index. - img, err = remote.Image(ref, - remote.WithContext(ctx), - remote.WithAuthFromKeychain(authn.DefaultKeychain), - remote.WithPlatform(requested)) + opts := registryRemoteOptions(ctx, credentials) + opts = append(opts, remote.WithPlatform(requested)) + img, err = remote.Image(ref, opts...) } else { // Single-arch manifest: the digest already pins the exact image. img, err = desc.Image() @@ -206,10 +223,18 @@ func (r *pullResult) measure(phase string, operation func() error) error { } func (c *ociClient) pullAndExport(ctx context.Context, imageRef, digest, exportDir string) (*pullResult, error) { - return c.pullAndExportWithPlatform(ctx, imageRef, digest, exportDir, vmPlatform()) + return c.pullAndExportWithAuth(ctx, imageRef, digest, exportDir, nil) +} + +func (c *ociClient) pullAndExportWithAuth(ctx context.Context, imageRef, digest, exportDir string, credentials *authn.AuthConfig) (*pullResult, error) { + return c.pullAndExportWithPlatformAuth(ctx, imageRef, digest, exportDir, vmPlatform(), credentials) } func (c *ociClient) pullAndExportWithPlatform(ctx context.Context, imageRef, digest, exportDir string, platform gcr.Platform) (*pullResult, error) { + return c.pullAndExportWithPlatformAuth(ctx, imageRef, digest, exportDir, platform, nil) +} + +func (c *ociClient) pullAndExportWithPlatformAuth(ctx context.Context, imageRef, digest, exportDir string, platform gcr.Platform, credentials *authn.AuthConfig) (*pullResult, error) { // Use a shared OCI layout for all images to enable automatic layer caching // The cacheDir itself is the OCI layout root with shared blobs/sha256/ directory // The digest is ALWAYS known at this point (from inspectManifest or digest reference) @@ -227,7 +252,7 @@ func (c *ociClient) pullAndExportWithPlatform(ctx context.Context, imageRef, dig if !result.CacheHit { // Not cached, pull it using digest-based tag if err := result.measure("registry_pull", func() error { - return c.pullToOCILayoutWithPlatform(ctx, imageRef, layoutTag, platform) + return c.pullToOCILayoutWithPlatformAuth(ctx, imageRef, layoutTag, platform, credentials) }); err != nil { return result, fmt.Errorf("pull to oci layout: %w", err) } @@ -265,6 +290,10 @@ func (c *ociClient) pullToOCILayout(ctx context.Context, imageRef, layoutTag str } func (c *ociClient) pullToOCILayoutWithPlatform(ctx context.Context, imageRef, layoutTag string, platform gcr.Platform) error { + return c.pullToOCILayoutWithPlatformAuth(ctx, imageRef, layoutTag, platform, nil) +} + +func (c *ociClient) pullToOCILayoutWithPlatformAuth(ctx context.Context, imageRef, layoutTag string, platform gcr.Platform, credentials *authn.AuthConfig) error { ref, err := name.ParseReference(imageRef) if err != nil { return fmt.Errorf("parse image reference: %w", err) @@ -273,10 +302,9 @@ func (c *ociClient) pullToOCILayoutWithPlatform(ctx context.Context, imageRef, l // Use system authentication (reads from ~/.docker/config.json, etc.) // Default retry: only on network errors, max ~1.3s total // WithPlatform ensures we pull the correct architecture for multi-arch images - img, err := remote.Image(ref, - remote.WithContext(ctx), - remote.WithAuthFromKeychain(authn.DefaultKeychain), - remote.WithPlatform(platform)) + opts := registryRemoteOptions(ctx, credentials) + opts = append(opts, remote.WithPlatform(platform)) + img, err := remote.Image(ref, opts...) if err != nil { // Rate limits fail here immediately (429 is not retried by default) return fmt.Errorf("fetch image manifest: %w", ClassifyRegistryError(err)) @@ -306,6 +334,14 @@ func (c *ociClient) pullToOCILayoutWithPlatform(ctx context.Context, imageRef, l return nil } +func registryRemoteOptions(ctx context.Context, credentials *authn.AuthConfig) []remote.Option { + opts := []remote.Option{remote.WithContext(ctx)} + if credentials == nil { + return append(opts, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + } + return append(opts, remote.WithAuth(authn.FromConfig(*credentials))) +} + // extractDigest gets the manifest digest from the OCI layout func (c *ociClient) extractDigest(layoutTag string) (string, error) { casEngine, err := dir.Open(c.cacheDir) diff --git a/lib/images/storage.go b/lib/images/storage.go index 49aebe2b..02a53d60 100644 --- a/lib/images/storage.go +++ b/lib/images/storage.go @@ -14,20 +14,21 @@ import ( ) type imageMetadata struct { - Name string `json:"name"` // Normalized ref (tag or digest) - Digest string `json:"digest"` // Always present: sha256:... - Platform string `json:"platform,omitempty"` - Status string `json:"status"` - Error *string `json:"error,omitempty"` - Request *CreateImageRequest `json:"request,omitempty"` - SizeBytes int64 `json:"size_bytes"` - Entrypoint []string `json:"entrypoint,omitempty"` - Cmd []string `json:"cmd,omitempty"` - Env map[string]string `json:"env,omitempty"` - Labels map[string]string `json:"labels,omitempty"` - Tags tags.Tags `json:"tags,omitempty"` - WorkingDir string `json:"working_dir,omitempty"` - CreatedAt time.Time `json:"created_at"` + Name string `json:"name"` // Normalized ref (tag or digest) + Digest string `json:"digest"` // Always present: sha256:... + Platform string `json:"platform,omitempty"` + Status string `json:"status"` + Error *string `json:"error,omitempty"` + Request *CreateImageRequest `json:"request,omitempty"` + SizeBytes int64 `json:"size_bytes"` + Entrypoint []string `json:"entrypoint,omitempty"` + Cmd []string `json:"cmd,omitempty"` + Env map[string]string `json:"env,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + Tags tags.Tags `json:"tags,omitempty"` + WorkingDir string `json:"working_dir,omitempty"` + CreatedAt time.Time `json:"created_at"` + BorrowedAuth bool `json:"borrowed_auth,omitempty"` } func (m *imageMetadata) toImage() *Image { diff --git a/lib/images/types.go b/lib/images/types.go index 4f3a2a4e..a7c49819 100644 --- a/lib/images/types.go +++ b/lib/images/types.go @@ -3,6 +3,7 @@ package images import ( "time" + "github.com/google/go-containerregistry/pkg/authn" "github.com/kernel/hypeman/lib/tags" ) @@ -31,4 +32,7 @@ type CreateImageRequest struct { // Platform selects which platform variant of a multi-arch image to pull as // os/arch[/variant] (e.g., linux/amd64). Empty means the host platform. Platform string + // Credentials are borrowed for this request's registry operations only. + // They intentionally have no JSON representation and must never be persisted. + Credentials *authn.AuthConfig `json:"-"` } diff --git a/lib/oapi/oapi.go b/lib/oapi/oapi.go index 05d7482f..91bc0a87 100644 --- a/lib/oapi/oapi.go +++ b/lib/oapi/oapi.go @@ -519,6 +519,11 @@ type CreateDeviceRequest struct { // CreateImageRequest defines model for CreateImageRequest. type CreateImageRequest struct { + // Credentials Docker-style registry credentials borrowed for one image pull or push request. They remain + // in memory and are never persisted or logged. When omitted or empty, the server's own registry + // credentials are used. An interrupted credentialed operation must be retried with fresh credentials. + Credentials *PushCredentials `json:"credentials,omitempty"` + // Name OCI image reference (e.g., docker.io/library/nginx:latest) Name string `json:"name"` @@ -701,12 +706,9 @@ type CreateInstanceRequestNetworkEgressEnforcementMode string // CreatePushRequest defines model for CreatePushRequest. type CreatePushRequest struct { - // Credentials Registry credentials borrowed for this push only, docker-style: the - // caller's registry login (e.g. from the client's ~/.docker/config.json) - // rides along with the request instead of living on the server. Never - // persisted or logged; a push interrupted by a restart fails instead of - // retrying without them. When omitted, the server's own registry - // credentials are used. + // Credentials Docker-style registry credentials borrowed for one image pull or push request. They remain + // in memory and are never persisted or logged. When omitted or empty, the server's own registry + // credentials are used. An interrupted credentialed operation must be retried with fresh credentials. Credentials *PushCredentials `json:"credentials,omitempty"` // Image Hypeman image name to push (tag or digest form) @@ -1381,7 +1383,7 @@ type PathInfo struct { // Push defines model for Push. type Push struct { - // Bytes Total compressed layer bytes pushed (only when status is pushed) + // Bytes Total compressed layer bytes pushed Bytes *int64 `json:"bytes,omitempty"` CompletedAt *time.Time `json:"completed_at"` CreatedAt time.Time `json:"created_at"` @@ -1389,7 +1391,7 @@ type Push struct { // Digest Cached manifest digest being pushed Digest string `json:"digest"` - // Error Error message (only when status is failed) + // Error Error message Error *string `json:"error"` // Id Push job identifier @@ -1398,10 +1400,10 @@ type Push struct { // Image Hypeman image name (normalized ref) Image string `json:"image"` - // Layers Number of layers pushed (only when status is pushed) + // Layers Number of layers pushed Layers *int `json:"layers,omitempty"` - // QueuePosition Position in the push queue (only when status is queued) + // QueuePosition Position in the push queue QueuePosition *int `json:"queue_position"` Status PushStatus `json:"status"` @@ -1409,17 +1411,14 @@ type Push struct { Target string `json:"target"` } -// PushCredentials Registry credentials borrowed for this push only, docker-style: the -// caller's registry login (e.g. from the client's ~/.docker/config.json) -// rides along with the request instead of living on the server. Never -// persisted or logged; a push interrupted by a restart fails instead of -// retrying without them. When omitted, the server's own registry -// credentials are used. +// PushCredentials Docker-style registry credentials borrowed for one image pull or push request. They remain +// in memory and are never persisted or logged. When omitted or empty, the server's own registry +// credentials are used. An interrupted credentialed operation must be retried with fresh credentials. type PushCredentials struct { // Password Registry password or access token Password *string `json:"password,omitempty"` - // RegistryToken Bearer token sent as-is in the Authorization header + // RegistryToken Bearer token for an Authorization header RegistryToken *string `json:"registry_token,omitempty"` // Username Registry username @@ -18477,349 +18476,347 @@ func (sh *strictHandler) GetVolume(w http.ResponseWriter, r *http.Request, id st // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "AQAA///3h2dLttABAA==", + "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", + "/v8AAAD//9ms+D+czwEA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/openapi.yaml b/openapi.yaml index 2b3a0f41..6e8f02b5 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1261,6 +1261,8 @@ components: pattern: '^[a-z0-9]+/[a-z0-9]+(/[a-z0-9.]+)?$' description: 'Target platform as os/arch[/variant] (e.g. "linux/amd64"), matching Docker --platform. Omit for the host platform. Not a fixed enum: the os/arch[/variant] grammar is validated server-side and invalid values return 400 invalid_platform. Only os "linux" with arch amd64 or arm64 is accepted today.' example: linux/amd64 + credentials: + $ref: "#/components/schemas/PushCredentials" tags: $ref: "#/components/schemas/Tags" @@ -1806,8 +1808,9 @@ components: PushCredentials: type: object description: | - Registry credentials borrowed for this push only. When omitted, the server's own registry - credentials are used. + Docker-style registry credentials borrowed for one image pull or push request. They remain + in memory and are never persisted or logged. When omitted or empty, the server's own registry + credentials are used. An interrupted credentialed operation must be retried with fresh credentials. properties: username: type: string From 2ed977bd54645b2cf3bfa52ad3423fc9e5fb02a1 Mon Sep 17 00:00:00 2001 From: chruffins <23645059+chruffins@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:55:05 +0000 Subject: [PATCH 2/3] Bound borrowed image pull credentials --- cmd/api/api/images.go | 5 + cmd/api/api/images_test.go | 8 ++ lib/images/credentials_test.go | 35 +++++++ lib/images/errors.go | 3 +- lib/images/manager.go | 151 ++++++++++++++++++++++++----- lib/oapi/oapi.go | 171 ++++++++++++++++++--------------- openapi.yaml | 6 ++ 7 files changed, 278 insertions(+), 101 deletions(-) diff --git a/cmd/api/api/images.go b/cmd/api/api/images.go index e8d1c050..dc003370 100644 --- a/cmd/api/api/images.go +++ b/cmd/api/api/images.go @@ -73,6 +73,11 @@ func (s *ApiService) CreateImage(ctx context.Context, request oapi.CreateImageRe Code: "rate_limited", Message: images.RateLimitMessage, }, nil + case errors.Is(err, images.ErrCredentialConflict): + return oapi.CreateImage409JSONResponse{ + Code: "credential_conflict", + Message: err.Error(), + }, nil case errors.Is(err, images.ErrNotFound): return oapi.CreateImage404JSONResponse{ Code: "not_found", diff --git a/cmd/api/api/images_test.go b/cmd/api/api/images_test.go index 3b785f27..bb13a9c0 100644 --- a/cmd/api/api/images_test.go +++ b/cmd/api/api/images_test.go @@ -94,6 +94,12 @@ func TestCreateImage_ErrorStatusMapping(t *testing.T) { wantType: oapi.CreateImage429JSONResponse{}, wantCode: "rate_limited", }, + { + name: "credential conflict -> 409", + err: fmt.Errorf("resolve: %w", images.ErrCredentialConflict), + wantType: oapi.CreateImage409JSONResponse{}, + wantCode: "credential_conflict", + }, { name: "not found -> 404", err: fmt.Errorf("resolve: %w", images.ErrNotFound), @@ -130,6 +136,8 @@ func errorCodeOf(resp oapi.CreateImageResponseObject) string { return r.Code case oapi.CreateImage404JSONResponse: return r.Code + case oapi.CreateImage409JSONResponse: + return r.Code case oapi.CreateImage429JSONResponse: return r.Code case oapi.CreateImage500JSONResponse: diff --git a/lib/images/credentials_test.go b/lib/images/credentials_test.go index 8f61282b..0d17ca54 100644 --- a/lib/images/credentials_test.go +++ b/lib/images/credentials_test.go @@ -81,6 +81,41 @@ func TestCreateImageRequestCredentialsAreNotPersisted(t *testing.T) { assert.Contains(t, string(data), `"borrowed_auth": true`) } +func TestInflightPullRejectsDifferentCredentials(t *testing.T) { + m := &manager{ + inflightPulls: make(map[string]*inflightImagePull), + borrowedCredentialsTimeout: time.Minute, + } + const digest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + credentials := &authn.AuthConfig{Username: "AWS", Password: "token-a"} + inflight := m.registerInflightPull(digest, credentials) + t.Cleanup(m.releaseInflightPull(digest, inflight)) + + assert.True(t, m.inflightCredentialsMatch(digest, &authn.AuthConfig{Username: "AWS", Password: "token-a"})) + assert.False(t, m.inflightCredentialsMatch(digest, nil)) + assert.False(t, m.inflightCredentialsMatch(digest, &authn.AuthConfig{Username: "AWS", Password: "token-b"})) +} + +func TestBorrowedCredentialsExpireWhileQueued(t *testing.T) { + m := &manager{ + inflightPulls: make(map[string]*inflightImagePull), + borrowedCredentialsTimeout: time.Millisecond, + } + const digest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + inflight := m.registerInflightPull(digest, &authn.AuthConfig{Username: "AWS", Password: "secret"}) + defer m.releaseInflightPull(digest, inflight)() + + require.Eventually(t, func() bool { + m.createMu.Lock() + defer m.createMu.Unlock() + return m.inflightPulls[digest].credentials == nil + }, time.Second, time.Millisecond) + + credentials, _, expired := m.borrowedAuth(digest) + assert.True(t, expired) + assert.Nil(t, credentials) +} + func TestRecoverInterruptedCredentialedPullFailsForFreshRetry(t *testing.T) { p := paths.New(t.TempDir()) client, err := newOCIClient(p.SystemOCICache()) diff --git a/lib/images/errors.go b/lib/images/errors.go index 22c56408..6a63741f 100644 --- a/lib/images/errors.go +++ b/lib/images/errors.go @@ -26,7 +26,8 @@ var ( // response (e.g. Docker Hub's unauthenticated pull limit). Transient and // caller-actionable (retry later or authenticate), not a server fault. ErrRateLimited = errors.New("registry rate limit exceeded") - ErrBorrowedCredentialsExpired = errors.New("credentialed image pull was interrupted; submit POST /images again with fresh credentials") + ErrBorrowedCredentialsExpired = errors.New("credentialed image pull expired or was interrupted; submit POST /images again with fresh credentials") + ErrCredentialConflict = errors.New("image pull already in flight with different credentials") ) // ClassifyRegistryError classifies a raw registry/go-containerregistry error into a diff --git a/lib/images/manager.go b/lib/images/manager.go index c0652dcb..d7cc5f07 100644 --- a/lib/images/manager.go +++ b/lib/images/manager.go @@ -2,6 +2,7 @@ package images import ( "context" + "crypto/sha256" "errors" "fmt" "log/slog" @@ -25,6 +26,8 @@ const ( StatusConverting = "converting" StatusReady = "ready" StatusFailed = "failed" + + DefaultBorrowedCredentialsTimeout = 30 * time.Minute ) // StatusEvent represents a terminal status change for image readiness notifications. @@ -53,19 +56,27 @@ type Manager interface { WaitForReady(ctx context.Context, name string) error } +type inflightImagePull struct { + fingerprint [32]byte + credentials *authn.AuthConfig + credentialsExpireAt time.Time + timer *time.Timer +} + type manager struct { - paths *paths.Paths - ociClient *ociClient - queue *queue.Queue - createMu sync.Mutex - diskUsageMu sync.RWMutex - diskUsageLoaded bool - readyImageBytes int64 - ociCacheBytes int64 - metrics *Metrics - inflightAuth map[string][32]byte // keyed by digest - readySubscribers map[string][]chan StatusEvent // keyed by digestHex - subscriberMu sync.RWMutex + paths *paths.Paths + ociClient *ociClient + queue *queue.Queue + createMu sync.Mutex + diskUsageMu sync.RWMutex + diskUsageLoaded bool + readyImageBytes int64 + ociCacheBytes int64 + metrics *Metrics + inflightPulls map[string]*inflightImagePull // keyed by digest + borrowedCredentialsTimeout time.Duration + readySubscribers map[string][]chan StatusEvent // keyed by digestHex + subscriberMu sync.RWMutex } // NewManager creates a new image manager. @@ -79,10 +90,12 @@ func NewManager(p *paths.Paths, maxConcurrentBuilds int, meter metric.Meter) (Ma } m := &manager{ - paths: p, - ociClient: ociClient, - queue: queue.New(maxConcurrentBuilds), - readySubscribers: make(map[string][]chan StatusEvent), + paths: p, + ociClient: ociClient, + queue: queue.New(maxConcurrentBuilds), + inflightPulls: make(map[string]*inflightImagePull), + borrowedCredentialsTimeout: DefaultBorrowedCredentialsTimeout, + readySubscribers: make(map[string][]chan StatusEvent), } // Initialize metrics if meter is provided @@ -98,14 +111,25 @@ func NewManager(p *paths.Paths, maxConcurrentBuilds int, meter metric.Meter) (Ma return m, nil } +func credentialsPresent(credentials *authn.AuthConfig) bool { + return credentials != nil && (credentials.Username != "" || credentials.Password != "" || credentials.Auth != "" || credentials.IdentityToken != "" || credentials.RegistryToken != "") +} + func cloneCredentials(credentials *authn.AuthConfig) *authn.AuthConfig { - if credentials == nil || (credentials.Username == "" && credentials.Password == "" && credentials.RegistryToken == "") { + if !credentialsPresent(credentials) { return nil } cloned := *credentials return &cloned } +func credentialFingerprint(credentials *authn.AuthConfig) [32]byte { + if !credentialsPresent(credentials) { + return [32]byte{} + } + return sha256.Sum256([]byte(credentials.Username + "\x00" + credentials.Password + "\x00" + credentials.Auth + "\x00" + credentials.IdentityToken + "\x00" + credentials.RegistryToken)) +} + func (m *manager) ListImages(ctx context.Context) ([]Image, error) { metas, err := listAllMetadata(m.paths) if err != nil { @@ -189,11 +213,16 @@ func (m *manager) CreateImage(ctx context.Context, req CreateImageRequest) (*Ima } else { // We have this digest already (ready, pending, pulling, or converting). // last-pull-wins: repoint the tag to it (see createTagSymlink). - if meta.Status == StatusReady && ref.Tag() != "" { - createTagSymlink(m.paths, ref.Repository(), ref.Tag(), ref.DigestHex()) + if meta.Status == StatusReady { + if ref.Tag() != "" { + createTagSymlink(m.paths, ref.Repository(), ref.Tag(), ref.DigestHex()) + } + return meta.toImage(), nil + } + if !m.inflightCredentialsMatch(ref.Digest(), req.Credentials) { + return nil, fmt.Errorf("%w: retry after the current pull completes", ErrCredentialConflict) } img := meta.toImage() - // Add queue position if pending if meta.Status == StatusPending { img.QueuePosition = m.queue.GetPosition(meta.Digest) } @@ -255,6 +284,69 @@ func (m *manager) ImportLocalImage(ctx context.Context, repo, reference, digest return m.createAndQueueImage(ref, CreateImageRequest{Name: imageRef}, hostPlatform()) } +func (m *manager) inflightCredentialsMatch(digest string, credentials *authn.AuthConfig) bool { + inflight := m.inflightPulls[digest] + var existingFingerprint [32]byte + if inflight != nil { + existingFingerprint = inflight.fingerprint + } + return existingFingerprint == credentialFingerprint(credentials) +} + +func (m *manager) registerInflightPull(digest string, credentials *authn.AuthConfig) *inflightImagePull { + if m.inflightPulls == nil { + m.inflightPulls = make(map[string]*inflightImagePull) + } + inflight := &inflightImagePull{ + fingerprint: credentialFingerprint(credentials), + credentials: credentials, + } + m.inflightPulls[digest] = inflight + if credentials == nil { + return inflight + } + if m.borrowedCredentialsTimeout <= 0 { + m.borrowedCredentialsTimeout = DefaultBorrowedCredentialsTimeout + } + inflight.credentialsExpireAt = time.Now().Add(m.borrowedCredentialsTimeout) + inflight.timer = time.AfterFunc(m.borrowedCredentialsTimeout, func() { + m.createMu.Lock() + defer m.createMu.Unlock() + if m.inflightPulls[digest] == inflight { + inflight.credentials = nil + } + }) + return inflight +} + +func (m *manager) releaseInflightPull(digest string, inflight *inflightImagePull) func() { + return func() { + m.createMu.Lock() + defer m.createMu.Unlock() + if m.inflightPulls[digest] != inflight { + return + } + delete(m.inflightPulls, digest) + if inflight.timer != nil { + inflight.timer.Stop() + } + inflight.credentials = nil + } +} + +func (m *manager) borrowedAuth(digest string) (*authn.AuthConfig, time.Time, bool) { + m.createMu.Lock() + defer m.createMu.Unlock() + inflight := m.inflightPulls[digest] + if inflight == nil || inflight.credentialsExpireAt.IsZero() { + return nil, time.Time{}, false + } + if inflight.credentials == nil || time.Now().After(inflight.credentialsExpireAt) { + return nil, inflight.credentialsExpireAt, true + } + return inflight.credentials, inflight.credentialsExpireAt, false +} + func (m *manager) createAndQueueImage(ref *ResolvedRef, req CreateImageRequest, requestedPlatform Platform) (*Image, error) { // Build one request value and reuse it for both the persisted metadata and // the queue entry so they never diverge. @@ -282,10 +374,23 @@ func (m *manager) createAndQueueImage(ref *ResolvedRef, req CreateImageRequest, return nil, fmt.Errorf("write initial metadata: %w", err) } - // Enqueue the build using digest as the queue key for deduplication + // Keep borrowed credentials outside the queued closure so their lifetime is + // bounded even when this job waits behind another pull. + inflight := m.registerInflightPull(ref.Digest(), req.Credentials) queuePos := m.queue.Enqueue(ref.Digest(), func() { - m.buildImage(context.Background(), ref, req.Credentials) - }, nil) + credentials, deadline, expired := m.borrowedAuth(ref.Digest()) + if expired { + m.updateStatusByDigest(ref, StatusFailed, ErrBorrowedCredentialsExpired) + return + } + ctx := context.Background() + if !deadline.IsZero() { + var cancel context.CancelFunc + ctx, cancel = context.WithDeadline(ctx, deadline) + defer cancel() + } + m.buildImage(ctx, ref, credentials) + }, m.releaseInflightPull(ref.Digest(), inflight)) img := meta.toImage() if queuePos > 0 { diff --git a/lib/oapi/oapi.go b/lib/oapi/oapi.go index 91bc0a87..f2c40652 100644 --- a/lib/oapi/oapi.go +++ b/lib/oapi/oapi.go @@ -6551,6 +6551,7 @@ type CreateImageResponse struct { JSON400 *Error JSON401 *Error JSON404 *Error + JSON409 *Error JSON429 *Error JSON500 *Error } @@ -9106,6 +9107,13 @@ func ParseCreateImageResponse(rsp *http.Response) (*CreateImageResponse, error) } response.JSON404 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 429: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -14651,6 +14659,15 @@ func (response CreateImage404JSONResponse) VisitCreateImageResponse(w http.Respo return json.NewEncoder(w).Encode(response) } +type CreateImage409JSONResponse Error + +func (response CreateImage409JSONResponse) VisitCreateImageResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + + return json.NewEncoder(w).Encode(response) +} + type CreateImage429JSONResponse Error func (response CreateImage429JSONResponse) VisitCreateImageResponse(w http.ResponseWriter) error { @@ -18740,83 +18757,83 @@ var swaggerSpec = []string{ "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", - "/v8AAAD//9ms+D+czwEA", + "nXVfet0BK1bH1TqdtWspQ5OITmfK3H+EdAJhPqpYbRZGuXUPo8yqugqsiI1ueowpg6eayPYCaU6EQq8P", + "jw39i0fqxkeId1ttKjnhtfR0fXv2qkdYwCHAMQvO8+uk9skXNpgM/5fSAO9/1z3CVDjq1IMmhfEz1t/E", + "oSITOtun/L+2fo7oWGCx+K+tn3GUUEb+a/sgwopItX5nzDK4r5Puvg2YR8x82n6hZaKBaGJTADhcofBn", + "b7XU+d3735TabyZ9I8U/o+t33b+N7l8k11L13y7FnRoApo8HuuHKmM1HbXj0HQ3jHpymliMLaBilW6Qc", + "D2PGpYJHjy810saj0ozjisdGS+9/viGXHh+OdY+PukBIqH8K+Os28+ie7gLcOO5dubX93v9FwEE8ptOU", + "p7KY1BRjFcyItAl/ESkL4MemdufHc6Pi/RVz6eA+j45716u/8/0dafzVBTXC21zordL53VttdX77vtb5", + "DRiiTYq0IPFdV0BkvSFG08EhtmXjEmpkPXbUNy6fLYLeakMlNxcQWBD7Q/Z/tP3xuyI4fv+Ty75KB4Ot", + "PfidsPn7n1wCFjtxrEIYVDaHRNyD347gFnUKOJJQEirP9ayOw1SYBdZzINj/cQZSfpHc3kJyXPjdQmpl", + "IRXItdxCsmtxtyZSGUj/3m0kx28+gls44m/TSvrGr0dKFpxMJxMaUMKgHAHktMpaPKCx5L7fjNwyl5HZ", + "+8hCMFFJE2ltRmZSa4WGnldAvfdAsuO85Mt9W4+u2OrjzMngia1eaO21XFtoNti+Nn4Y3O/pdf+G2mNm", + "MWMR1UmXaKXbU1TElNOJUwVBsDkMEUQZI2HMmqzFPjrMss9lmiRcKGlK8oCFYIp2zrSF4CvfU67I4yvB", + "A2VnKJHdIYOirPqxQdHYuCQLU3CHcpbV1slmauvW+HL9ygWPHnQbfXkl1F/NqZUSes/b2Nbnezgl9MFE", + "x72oe8elsqdr2cYAi3tMsp3Ms2RS+oGy6fqjing2wiqbWwE0zaNqbeBUcVe8f2PGDX6SH0LuNMIBIMjp", + "1wy4kc1ONmhmxaYg5VjwKCLCgFYlqXLVvYYsGxxlherFtpTGhW5+lDJFo4uuiaYB5AGJMFtY5JYhK3WG", + "lSJxogWbxSKCEQqSmBFXyprpQVOeSniriyQvdYlwdIUXcsgEmUQksHODEpCCBAbfLYr66BcO6d4ITzFl", + "NgNZv2mKgv0gh+yChhEZ2WztC0QlkjMuFGEkRDGfE1nul2ARUSJgEodYU06iGC8ANskgyBn68IQYaKJS", + "TjjX/8YspFAyS/ecTXl/yDDaGgxQTDCTiELasMQTor+ybSAYRGlAPyKMdgbP7FeVdQNoT0f+Nb1fhCBz", + "HuBxtEBEczGciGodFjDbX1B9Ui/fhApp1itzL9raRKWFpdJV0Qy7KGUBFHlMhf4XFyhl9njVLQrIhId5", + "2ks4QkVWHM2m7Y9JgDU9GS/3A4BpPAhS4Tsc9VIXyvf9JyqZhemdA6l80knTAcGeCmHNGVcz2NMcttL6", + "jw1clTPVt3HIeDcJFwijAl/nDgWodM2maA0Axi7ywmDM1Za8WP/R7R29fa0gcNvfQHw9lvMJmIhPJqUN", + "uPpoMht4WXpFnYW/1X166CpCFkVcSPGUcalo4IRhtWbxd+OxtfG4nLJebp5wcVnUrcr8+zMXl22tr3NX", + "iP9RGWHFGX6F9wB6eAAR+/DXAeCMNoaKZpp7N9Cq/JXtUlC6qJIuzpijiLOp3kW5U/zevfYViy6IUlDL", + "nSnnnCDaCBnZH01RST0ZW7IPPPyBbfWhZZHu/R7ugn7jCtE4iUhMoOhkzzCbXuxMqzY1oalEs6za381k", + "pd5VxdRhYwtKc/3fdeoQ8JVbsDXQ3uvL5RWqEZ+uhgvLOnfYWB68sCEzNauJK3B9gTIZrBVaA86NrmY0", + "mAF2GNitun0DLYaT5CKDTV3fRy9hIxfRY6HzNQPJrXlN8ogYSLB5HF/s18sqvjs5gY8MbJgpoHixj1wp", + "xez8kPqtIhaYnkWEpUK/WYSztcwYhxW9UFjbm9n81i1KWA5rO2Q+xDBGrmyDdIIuCuBhFw3oYU7evuLT", + "B1PGus1g5GYuiiNrOgJvEhZ2mmIsaOTHDdscDHwYuS0xzMww7hjCrDaYV3yaAaGXWBknSVv2tcMELp7H", + "8RIeRmu5BEFShTxVf5MqJELAx5a7m5gbreHAFsHBl5pRmZFKbmOvA/t5I4kMMrGXVFqodrodwtK4s/+7", + "/dc8jjvdjh1PAdH4Bsr9Ciy4aoP1iBe9MgXAt+9q+U2g3MrCvoDlVjk5rDndrJGfmRe++ZtF57N7QDYE", + "/aDixP2aVNDCeMsOH8aRZDiRM64eF3qUdTVVtLZmV42bZU8PL0xdpY42IRzn9tNz9+VXYP2uiuxwY0Zu", + "uvce4lEfwWPOhJW12Uy4qEIOrYr9+OoZ6cstSW2qbTjkO2/e3M/XijG1pl4XEfaD0FRuwqniMVY0gKoh", + "wYxzWWD7MZnhOeX2qtTdWWWcCc4NY2faEPoLzaoX1hF8YRX5feu0Qrj4yPbRh89t4L3/C/co/+Lngl2e", + "SfyuU74BWRvKGgtKJijBqSRar0pjgoJFoKWiKRNDcDBDAU5UKghUwCIopozGaVxwNWjDScwxgFRcbMYX", + "XTROFYqwmIJdZB6acHpBAh7HhIUEPGRDNiN4TrVRJ1CEFWHBoicJVM6cE3TFxWXEcQhGvo3CMZW3BNEc", + "SDnropgoHGKFQdW40Dt+ZLJ4LrJimsawZuQ654ZwyETKfjRo4LrZCzfQC0SkwuOIyllWdC3AIWGBF2r7", + "/OsWY1/eG3xOVHWiDxSXcytZ+pCBOkWvpxvO1xHD88iCkbmwy9hGzC9RemWzEVlOf3Bs9J+5pc1c3Rwf", + "6IonI/GyXfx13O1kTPfV3O88/AUOFyhMTXeFXQls/q3eymQCpRjuBKmVZhlvezWTVZfKyHwjmbfx0f15", + "fAtv2lciCbuNhn1THZN80l+DyLVUvZXMfSA3ovUlFbxiDyiCXUzVg6lPXBSk3GNxd1qBbbZmJreL0kkJ", + "DNYXZ9/FdlVs25CD24pt55utXaoXBDllPYjS9Etw68ZtFNXWdfAfmgtSmV1BZD64iMzvDu5NLB5ngtCI", + "xgQvIo7DbyFMd8kNTsCFMPgPgCjxmPBHC17DYoA++Oa6mYToutzKdycn601SQqilMkKoRywhCkkx+rPY", + "V+p/ToSgoSthfnhyZANmqUQiZX30OqZQV/ySkCTPKQEgj76en0PCqBdjLkFedDuEKbFIOGVq5SjyV+9m", + "MJ9uVcL5nuWkBbT+fiHd+kIaPPuPT5yBlIGsCTOB5ZapwqoxFNCFxlFmKrRrvQyPeapb1zJIk0mv5xRO", + "wQmNiFxIRWITFzhJI9huUBzB1s6035lV7kJUrN45JmEtISKmUlLO5JDZbI2ECN23/ly3Xwhx8l4IKJzJ", + "11MjJL+O8Dk9GBMxhlUT1QCzCCrXd/Y7GzhJNkKscEOIlh3eZwzpZ4iHQ3IRj3lEAxRRdinRWkQvjXmC", + "5hJF+o/1pQF1I/juS1cGvf3O0pQ+ZhPuLZ5meDZj5m8qr8qKNXcx+ejE2ktS3CxO/sBC+8WaXCnXBMFR", + "T9GYZMg1KFU0oh+MqNONUKloYJJ+csiCdyc5asGQnRAl9DsYksuiiATKOWw2EsGDjWE6GGwHCQX4s20C", + "gwOB1/w4hh4PT9+aRFASc7HoDpn+BzT85uDU3O5OsPUmFAbKiLri4hIdb7xeEWJ8DmT6D47RMxNcih3g", + "XfDvV4I3RwRp3EOyYYvyZJmpxJNvPojUanDf/QqP068AkEzZbNamAgegFMtZqkJ+xfw+hDmP0lj/w/xx", + "vArYS+Fg9g5e/Wq0XTOcld24CT6KTWnnFBJT3PFBLj0MwR5rzKomnJsCKDGlaEDvKXCgvkXu/vLu+yId", + "v8LrTktRVzj1q9lb933y2TE4jIsiPR7LNjec5mai+HLv0xWmzd6n5xEPLqUFQym6DbXdBgDj+sccENpe", + "EYKaALmZyIIIIXKdUAHIbxUHpMHckQgjRURMGY42YM6mEYC2dl4sPOcUUqSDiEKSGg0BtSgCdLqrGWFI", + "zwYcVa6Bwo2utKWliu8ULyMVR2MS8Jg4uO91n+n2d0zVz1yUsbu/Frn4pkB/PR89VT3PFXDlzT1+Fnz5", + "Cb6GUOkwtRfKbkRrL3n+o3EFdRGszbCzPZDDThcNO1vxsKNX4BCDCxUrtItiylJFZB8dGf8WJMHuDZAk", + "AWehdKjjzoO3PZBNKbGGLRvyK/fgu/tUeyxXASnPbCc+8aDfQ/p7SNpBa8UNZ/dk2IVNFyKeKuPut/vK", + "vhUSBe6R9Xu/qy3ske+2fRtJ/ne7fUsyClZZi8vC0hvJnqRyRppdbq9MJZ9UjQHN2lXXlDP0Bx/LLmLk", + "ynjDhVT9mtzTX5+aDu4DaV93dROUfTv37xD7LSD2c1r54RJNgKU+kh13GMxEcm0QYbFLe7c8BJYEYDfw", + "AEfo9eHxkAVaFBlwP0FiDtLJAoKbU/jg7+foxeFZFx1BpUf0Szpe76PXLFq4etvmjmbIjCZmhFeAGRob", + "riWh73g2Ywfuuctgcd3BA5VONjvDc7Pi1soFiXc7M4JD0Eg+dl5x05kHI/jsld5Aev3sl9myd5YqH50z", + "osSidzBRRNSbPbF5UixDrbCHtAOBs4qbgZ7UHUqHfZb3aXQDA06xvdXxYFV8+l714O4rhN7PLZmJEzH1", + "5sYpYH0ySDLA4eJxxTLJGcqEo08EFo/rrG5AU5awlWVLDQzosiny+ytyuS+VXSUk+P/U3QUzfbQXTUlp", + "nTQTZ/VGVt70uuTgmQEkthdVAU5wQNWii3AU2TPKngRZREovU3/HguDLkF+x/pCdZZVObEIvOjx923UX", + "tSik8tK0YO9i++j1nAiZjrPBIdho5tYYaE7CIVMcBTgK0kirG2QyIQHk4kIBE9lwl5sNpXOHeyfvxFtt", + "pRDVnj66Im9+noDVy9miynEbZqk3BAkiTONm+G+rqEHAIYQajHWjnCHKJpENqQoElxLZpnokolM6jmyA", + "kOyjNzOCJI7JkCURZowIlEoTFa+H3ksEkTI1Cd66AYDJNRzVRTm0XyK4sqEJEedCmmgCzeHvTpBUJFnC", + "Zmem5ROY8x3ptqZx29MDOakrY2h2hdhXkF4QwymG4JqP0sgFMN5rKLoZ0ENriY9l478RdDolQu8KbISs", + "Cccz29qR02z6UsZyY8HH8+ytdgUfs1YLWYmFjL2l0GyjHO067Nws6s/T+SVtRO+zj26WRfyr/qhl3+Vs", + "Vf8g7KPPnOW3Ukf/vJAk2NaBlXP4Y3MnFUZe2qqlRNvVsFqtM2vvMtO1NX7Wg8FmPWa0LFxKn20yeL8+", + "RhjcL8rDfZdEe9y8VUK7KtmmDSn/q/HsvwoOvBsg+wdGObkFkP1XlXcPSOMPh3/i3agPlUdfunt21Wa/", + "eSz6u0qfN4D0AMfWlD5vpJ4NXl1qKL2z77Qzk2yL35IGb+Mdb6C/O7J/t/pbmAwFYq26gtYMT+JELVxA", + "m72rzIPOJP1A+g0XwVnc6t1dBd8ipPPLsYfj08aAzm+zOPyDxIza4n1UouMjT9X1R4YxWNxzpYNlQ586", + "PSyCGZ2TZqd7eQdbEiWC9BKewOVKaAhm6eHOMoVFf/oB2eYt5qr9F1R/BLB8EqKQChKoaGEqcWqJYPr4", + "QSLBtSUAz7lYNEeJmC3ys+DxgZ3NivPQ7inrDMvjDONFL8QK9+ZO2ixxoX1GdKeLp9QCD1GGXj5Ha+Ra", + "CVNjAk205YPoJCOpKbcvgSfXiwPeHDR4NukHMpqO24xySbWQ17YaCwpSqXjs1v74CK1B9bEpYXottKo/", + "AU02EXxOQxKWxtiZ88hQdbOBoDf1u2qlIisd54wLM7gH0WHaHEjTDzQpi4UsJGZMGYbBrazLUd5TJolf", + "94cpcwE4do3cKL4fYdbyW3PGjuZEqIRpiag4NxDP69+Pucd8zBWTodyZVjrtXHjOcud1u/yolmlLd1H4", + "Icudu1+39buvJ6WHykeZzWNd5/PMIG1ym39dLDi4v/Phvt3l7x5xCuhL4ozvgqscGtAt+hjmFcR0h2RO", + "Ip7EUJEc3u10O6mIOvudmVLJ/sYGxH7PuFT7O8+ebHc+vf/0/wcAAP//eC/k9kLQAQA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/openapi.yaml b/openapi.yaml index 6e8f02b5..fcaa132f 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2395,6 +2395,12 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + 409: + description: An image pull is already in flight with different credentials + content: + application/json: + schema: + $ref: "#/components/schemas/Error" 401: description: Unauthorized content: From 2846289a115057febf3a3889318da02a6416ea3a Mon Sep 17 00:00:00 2001 From: chruffins <23645059+chruffins@users.noreply.github.com> Date: Thu, 13 Aug 2026 15:12:54 +0000 Subject: [PATCH 3/3] Resume cached image conversion after restart --- lib/images/credentials_test.go | 36 +++++++++++++++++++++++++++++++ lib/images/manager.go | 39 +++++++++++++++------------------- 2 files changed, 53 insertions(+), 22 deletions(-) diff --git a/lib/images/credentials_test.go b/lib/images/credentials_test.go index 0d17ca54..059dac6a 100644 --- a/lib/images/credentials_test.go +++ b/lib/images/credentials_test.go @@ -13,6 +13,8 @@ import ( "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/registry" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/layout" "github.com/google/go-containerregistry/pkg/v1/random" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/kernel/hypeman/lib/paths" @@ -116,6 +118,40 @@ func TestBorrowedCredentialsExpireWhileQueued(t *testing.T) { assert.Nil(t, credentials) } +func TestRecoverInterruptedCredentialedConversionFromCache(t *testing.T) { + p := paths.New(t.TempDir()) + img := createTestDockerImage(t) + digest, err := img.Digest() + require.NoError(t, err) + digestString := digest.String() + + require.NoError(t, os.MkdirAll(p.SystemOCICache(), 0o755)) + cache, err := layout.Write(p.SystemOCICache(), empty.Index) + require.NoError(t, err) + require.NoError(t, cache.AppendImage(img, layout.WithAnnotations(map[string]string{ + "org.opencontainers.image.ref.name": digestToLayoutTag(digestString), + }))) + + const repository = "registry.example/private/image" + meta := &imageMetadata{ + Name: repository + ":latest", + Digest: digestString, + Status: StatusConverting, + BorrowedAuth: true, + Request: &CreateImageRequest{Name: repository + ":latest"}, + CreatedAt: time.Now(), + } + require.NoError(t, writeMetadata(p, repository, strings.TrimPrefix(digestString, "sha256:"), meta)) + + mgr, err := NewManager(p, 1, nil) + require.NoError(t, err) + waitForReady(t, mgr, context.Background(), meta.Name) + + recovered, err := mgr.GetImage(context.Background(), meta.Name) + require.NoError(t, err) + assert.Equal(t, StatusReady, recovered.Status) +} + func TestRecoverInterruptedCredentialedPullFailsForFreshRetry(t *testing.T) { p := paths.New(t.TempDir()) client, err := newOCIClient(p.SystemOCICache()) diff --git a/lib/images/manager.go b/lib/images/manager.go index d7cc5f07..8e3e84ad 100644 --- a/lib/images/manager.go +++ b/lib/images/manager.go @@ -596,29 +596,24 @@ func (m *manager) RecoverInterruptedBuilds() { }) for _, meta := range metas { - switch meta.Status { - case StatusPending, StatusPulling, StatusConverting: - if meta.BorrowedAuth { - normalized, parseErr := ParseNormalizedRef(meta.Name) - if parseErr == nil { - ref := NewResolvedRef(normalized, meta.Digest) - m.updateStatusByDigest(ref, StatusFailed, ErrBorrowedCredentialsExpired) - } - continue - } - if meta.Request != nil && meta.Digest != "" { - metaCopy := meta - normalized, err := ParseNormalizedRef(metaCopy.Name) - if err != nil { - continue - } - // Create a ResolvedRef since we already have the digest from metadata - ref := NewResolvedRef(normalized, metaCopy.Digest) - m.queue.Enqueue(metaCopy.Digest, func() { - m.buildImage(context.Background(), ref, nil) - }, nil) - } + if meta.Status != StatusPending && meta.Status != StatusPulling && meta.Status != StatusConverting { + continue + } + if meta.Request == nil || meta.Digest == "" { + continue + } + normalized, err := ParseNormalizedRef(meta.Name) + if err != nil { + continue + } + ref := NewResolvedRef(normalized, meta.Digest) + if meta.BorrowedAuth && (meta.Status != StatusConverting || !m.ociClient.existsInLayout(digestToLayoutTag(meta.Digest))) { + m.updateStatusByDigest(ref, StatusFailed, ErrBorrowedCredentialsExpired) + continue } + m.queue.Enqueue(meta.Digest, func() { + m.buildImage(context.Background(), ref, nil) + }, nil) } }