diff --git a/cmd/api/api/api.go b/cmd/api/api/api.go index 20db9461..8a2c72db 100644 --- a/cmd/api/api/api.go +++ b/cmd/api/api/api.go @@ -7,6 +7,7 @@ import ( "github.com/kernel/hypeman/lib/builds" "github.com/kernel/hypeman/lib/devices" "github.com/kernel/hypeman/lib/guestmemory" + "github.com/kernel/hypeman/lib/imagepush" "github.com/kernel/hypeman/lib/images" "github.com/kernel/hypeman/lib/ingress" "github.com/kernel/hypeman/lib/instances" @@ -28,6 +29,7 @@ type ApiService struct { DeviceManager devices.Manager IngressManager ingress.Manager BuildManager builds.Manager + PushManager imagepush.Manager ResourceManager *resources.Manager GuestMemoryController guestmemory.Controller AutoStandbyController *autostandby.Controller @@ -47,6 +49,7 @@ func New( deviceManager devices.Manager, ingressManager ingress.Manager, buildManager builds.Manager, + pushManager imagepush.Manager, resourceManager *resources.Manager, guestMemoryController guestmemory.Controller, autoStandbyController *autostandby.Controller, @@ -62,6 +65,7 @@ func New( DeviceManager: deviceManager, IngressManager: ingressManager, BuildManager: buildManager, + PushManager: pushManager, ResourceManager: resourceManager, GuestMemoryController: guestMemoryController, AutoStandbyController: autoStandbyController, diff --git a/cmd/api/api/pushes.go b/cmd/api/api/pushes.go new file mode 100644 index 00000000..f409611e --- /dev/null +++ b/cmd/api/api/pushes.go @@ -0,0 +1,169 @@ +package api + +import ( + "context" + "errors" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/kernel/hypeman/lib/imagepush" + "github.com/kernel/hypeman/lib/images" + "github.com/kernel/hypeman/lib/logger" + "github.com/kernel/hypeman/lib/oapi" +) + +const pushRetryAfterSeconds = 2 + +func (s *ApiService) CreatePush(ctx context.Context, request oapi.CreatePushRequestObject) (oapi.CreatePushResponseObject, error) { + if request.Body == nil { + return oapi.CreatePush400JSONResponse{ + Code: "invalid_request", + Message: "request body is required", + }, nil + } + + log := logger.FromContext(ctx) + + domainReq := imagepush.PushRequest{ + Image: request.Body.Image, + Target: request.Body.Target, + Credentials: pushCredentialsToAuthn(request.Body.Credentials), + } + if request.Body.Insecure != nil { + domainReq.Insecure = *request.Body.Insecure + } + + push, err := s.PushManager.CreatePush(ctx, domainReq) + if err != nil { + switch { + case errors.Is(err, images.ErrInvalidName): + return oapi.CreatePush400JSONResponse{ + Code: "invalid_name", + Message: err.Error(), + }, nil + case errors.Is(err, imagepush.ErrInvalidTarget): + return oapi.CreatePush400JSONResponse{ + Code: "invalid_target", + Message: err.Error(), + }, nil + case errors.Is(err, images.ErrNotFound): + return oapi.CreatePush404JSONResponse{ + Code: "not_found", + Message: "image not found", + }, nil + case errors.Is(err, imagepush.ErrNotFound): + return oapi.CreatePush409JSONResponse{ + Code: "conflict", + Message: err.Error(), + }, nil + case errors.Is(err, imagepush.ErrCredentialConflict): + return oapi.CreatePush409JSONResponse{ + Code: "credential_conflict", + Message: err.Error(), + }, nil + case errors.Is(err, imagepush.ErrImageNotReady): + return oapi.CreatePush409JSONResponse{ + Code: "image_not_ready", + Message: err.Error(), + }, nil + default: + log.ErrorContext(ctx, "failed to create push", "error", err) + return oapi.CreatePush500JSONResponse{ + Code: "internal_error", + Message: "failed to create push", + }, nil + } + } + + return oapi.CreatePush202JSONResponse{ + Body: pushToOAPI(*push), + Headers: oapi.CreatePush202ResponseHeaders{ + Location: "/pushes/" + push.ID, + RetryAfter: pushRetryAfterSeconds, + }, + }, nil +} + +func (s *ApiService) GetPush(ctx context.Context, request oapi.GetPushRequestObject) (oapi.GetPushResponseObject, error) { + log := logger.FromContext(ctx) + + push, err := s.PushManager.GetPush(ctx, request.Id) + if err != nil { + if errors.Is(err, imagepush.ErrNotFound) { + return oapi.GetPush404JSONResponse{ + Code: "not_found", + Message: "push not found", + }, nil + } + log.ErrorContext(ctx, "failed to get push", "error", err) + return oapi.GetPush500JSONResponse{ + Code: "internal_error", + Message: "failed to get push", + }, nil + } + + return oapi.GetPush200JSONResponse(pushToOAPI(*push)), nil +} + +func (s *ApiService) ListPushes(ctx context.Context, request oapi.ListPushesRequestObject) (oapi.ListPushesResponseObject, error) { + log := logger.FromContext(ctx) + + pushes, err := s.PushManager.ListPushes(ctx) + if err != nil { + log.ErrorContext(ctx, "failed to list pushes", "error", err) + return oapi.ListPushes500JSONResponse{ + Code: "internal_error", + Message: "failed to list pushes", + }, nil + } + + out := make([]oapi.Push, 0, len(pushes)) + for _, push := range pushes { + out = append(out, pushToOAPI(push)) + } + 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 { + if creds == nil { + return nil + } + cfg := &authn.AuthConfig{} + if creds.Username != nil { + cfg.Username = *creds.Username + } + if creds.Password != nil { + cfg.Password = *creds.Password + } + if creds.RegistryToken != nil { + cfg.RegistryToken = *creds.RegistryToken + } + if cfg.Username == "" && cfg.Password == "" && cfg.RegistryToken == "" { + return nil + } + return cfg +} + +func pushToOAPI(push imagepush.Push) oapi.Push { + out := oapi.Push{ + Id: push.ID, + Image: push.Image, + Digest: push.Digest, + Target: push.Target, + Status: oapi.PushStatus(push.Status), + QueuePosition: push.QueuePosition, + Error: push.Error, + CreatedAt: push.CreatedAt, + CompletedAt: push.CompletedAt, + } + if push.Status == imagepush.StatusPushed { + layers := push.Layers + out.Layers = &layers + bytes := push.Bytes + out.Bytes = &bytes + } + return out +} diff --git a/cmd/api/api/pushes_test.go b/cmd/api/api/pushes_test.go new file mode 100644 index 00000000..adb478d8 --- /dev/null +++ b/cmd/api/api/pushes_test.go @@ -0,0 +1,296 @@ +package api + +import ( + "context" + "fmt" + "net/http/httptest" + "testing" + "time" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/kernel/hypeman/lib/imagepush" + "github.com/kernel/hypeman/lib/images" + "github.com/kernel/hypeman/lib/oapi" + "github.com/stretchr/testify/require" +) + +// fakePushManager implements imagepush.Manager for handler tests. +type fakePushManager struct { + createErr error + createdReq imagepush.PushRequest + push *imagepush.Push + getErr error + listErr error + pushes []imagepush.Push +} + +func (f *fakePushManager) CreatePush(_ context.Context, req imagepush.PushRequest) (*imagepush.Push, error) { + f.createdReq = req + if f.createErr != nil { + return nil, f.createErr + } + return f.push, nil +} + +func (f *fakePushManager) GetPush(_ context.Context, _ string) (*imagepush.Push, error) { + if f.getErr != nil { + return nil, f.getErr + } + return f.push, nil +} + +func (f *fakePushManager) ListPushes(_ context.Context) ([]imagepush.Push, error) { + if f.listErr != nil { + return nil, f.listErr + } + return f.pushes, nil +} + +func (f *fakePushManager) WaitForPush(_ context.Context, _ string) error { return nil } + +func (f *fakePushManager) LiveCacheManifestDigests() []string { return nil } + +func TestCreatePush_RejectsMissingBody(t *testing.T) { + t.Parallel() + + svc := &ApiService{PushManager: &fakePushManager{}} + resp, err := svc.CreatePush(context.Background(), oapi.CreatePushRequestObject{}) + require.NoError(t, err) + got, ok := resp.(oapi.CreatePush400JSONResponse) + require.True(t, ok) + require.Equal(t, "invalid_request", got.Code) +} + +func TestCreatePush_MapsRequestAndCredentials(t *testing.T) { + t.Parallel() + + now := time.Now().Truncate(time.Second) + fake := &fakePushManager{push: &imagepush.Push{ + ID: "push-1", + Image: "docker.io/library/alpine:latest", + Digest: "sha256:abc", + Target: "registry.example.com/app:v1", + Status: imagepush.StatusQueued, + CreatedAt: now, + }} + svc := &ApiService{PushManager: fake} + + insecure := true + username, password, token := "pusher", "hunter2", "bearer-tok" + resp, err := svc.CreatePush(context.Background(), oapi.CreatePushRequestObject{ + Body: &oapi.CreatePushRequest{ + Image: "alpine:latest", + Target: "registry.example.com/app:v1", + Insecure: &insecure, + Credentials: &oapi.PushCredentials{ + Username: &username, + Password: &password, + RegistryToken: &token, + }, + }, + }) + require.NoError(t, err) + require.IsType(t, oapi.CreatePush202JSONResponse{}, resp) + + got := resp.(oapi.CreatePush202JSONResponse) + require.Equal(t, "push-1", got.Body.Id) + require.Equal(t, oapi.PushStatus(imagepush.StatusQueued), got.Body.Status) + require.Equal(t, now, got.Body.CreatedAt) + require.Equal(t, "/pushes/push-1", got.Headers.Location) + require.Equal(t, int32(pushRetryAfterSeconds), got.Headers.RetryAfter) + + recorder := httptest.NewRecorder() + require.NoError(t, got.VisitCreatePushResponse(recorder)) + require.Equal(t, 202, recorder.Code) + require.Equal(t, "/pushes/push-1", recorder.Header().Get("Location")) + require.Equal(t, fmt.Sprint(pushRetryAfterSeconds), recorder.Header().Get("Retry-After")) + + // Borrowed credentials must reach the manager as an auth config. + require.Equal(t, "alpine:latest", fake.createdReq.Image) + require.True(t, fake.createdReq.Insecure) + require.NotNil(t, fake.createdReq.Credentials) + require.Equal(t, &authn.AuthConfig{ + Username: "pusher", + Password: "hunter2", + RegistryToken: "bearer-tok", + }, fake.createdReq.Credentials) +} + +func TestCreatePush_NoCredentialsStaysNil(t *testing.T) { + t.Parallel() + + fake := &fakePushManager{push: &imagepush.Push{ID: "push-1", Status: imagepush.StatusQueued}} + svc := &ApiService{PushManager: fake} + + resp, err := svc.CreatePush(context.Background(), oapi.CreatePushRequestObject{ + Body: &oapi.CreatePushRequest{Image: "alpine:latest", Target: "registry.example.com/app:v1"}, + }) + require.NoError(t, err) + require.IsType(t, oapi.CreatePush202JSONResponse{}, resp) + require.Nil(t, fake.createdReq.Credentials) +} + +func TestCreatePush_EmptyCredentialsFallsBackToDefault(t *testing.T) { + t.Parallel() + + fake := &fakePushManager{push: &imagepush.Push{ID: "push-1", Status: imagepush.StatusQueued}} + svc := &ApiService{PushManager: fake} + + // An empty credentials object must behave like no credentials at all: + // the server's default resolution stays in charge. + resp, err := svc.CreatePush(context.Background(), oapi.CreatePushRequestObject{ + Body: &oapi.CreatePushRequest{ + Image: "alpine:latest", + Target: "registry.example.com/app:v1", + Credentials: &oapi.PushCredentials{}, + }, + }) + require.NoError(t, err) + require.IsType(t, oapi.CreatePush202JSONResponse{}, resp) + require.Nil(t, fake.createdReq.Credentials) +} + +func TestCreatePush_ErrorStatusMapping(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + err error + wantType any + wantCode string + }{ + { + name: "invalid name -> 400", + err: fmt.Errorf("lookup: %w", images.ErrInvalidName), + wantType: oapi.CreatePush400JSONResponse{}, + wantCode: "invalid_name", + }, + { + name: "invalid target -> 400", + err: fmt.Errorf("parse: %w", imagepush.ErrInvalidTarget), + wantType: oapi.CreatePush400JSONResponse{}, + wantCode: "invalid_target", + }, + { + name: "image not found -> 404", + err: fmt.Errorf("lookup: %w", images.ErrNotFound), + wantType: oapi.CreatePush404JSONResponse{}, + wantCode: "not_found", + }, + { + name: "credential conflict -> 409", + err: fmt.Errorf("dedupe: %w", imagepush.ErrCredentialConflict), + wantType: oapi.CreatePush409JSONResponse{}, + wantCode: "credential_conflict", + }, + { + name: "image not ready -> 409", + err: fmt.Errorf("lookup: %w", imagepush.ErrImageNotReady), + wantType: oapi.CreatePush409JSONResponse{}, + wantCode: "image_not_ready", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + svc := &ApiService{PushManager: &fakePushManager{createErr: tc.err}} + + resp, err := svc.CreatePush(context.Background(), oapi.CreatePushRequestObject{ + Body: &oapi.CreatePushRequest{Image: "alpine:latest", Target: "registry.example.com/app:v1"}, + }) + require.NoError(t, err) + require.IsType(t, tc.wantType, resp) + require.Equal(t, tc.wantCode, pushErrorCodeOf(resp)) + }) + } +} + +func TestCreatePush_FinalizationConflict(t *testing.T) { + t.Parallel() + + svc := &ApiService{PushManager: &fakePushManager{ + createErr: fmt.Errorf("%w: push job is being finalized; retry", imagepush.ErrNotFound), + }} + resp, err := svc.CreatePush(context.Background(), oapi.CreatePushRequestObject{ + Body: &oapi.CreatePushRequest{Image: "alpine:latest", Target: "registry.example.com/app:v1"}, + }) + require.NoError(t, err) + got, ok := resp.(oapi.CreatePush409JSONResponse) + require.True(t, ok) + require.Equal(t, "conflict", got.Code) + require.Contains(t, got.Message, "retry") +} + +func TestGetPush_NotFound(t *testing.T) { + t.Parallel() + + svc := &ApiService{PushManager: &fakePushManager{getErr: imagepush.ErrNotFound}} + resp, err := svc.GetPush(context.Background(), oapi.GetPushRequestObject{Id: "missing"}) + require.NoError(t, err) + require.IsType(t, oapi.GetPush404JSONResponse{}, resp) + require.Equal(t, "not_found", pushErrorCodeOf(resp)) +} + +func TestGetPush_OmitsEmptyCounters(t *testing.T) { + t.Parallel() + + fake := &fakePushManager{push: &imagepush.Push{ + ID: "push-1", + Status: imagepush.StatusQueued, + }} + svc := &ApiService{PushManager: fake} + resp, err := svc.GetPush(context.Background(), oapi.GetPushRequestObject{Id: "push-1"}) + require.NoError(t, err) + got, ok := resp.(oapi.GetPush200JSONResponse) + require.True(t, ok) + require.Nil(t, got.Layers) + require.Nil(t, got.Bytes) +} + +func TestListPushes_Empty(t *testing.T) { + t.Parallel() + + svc := &ApiService{PushManager: &fakePushManager{}} + resp, err := svc.ListPushes(context.Background(), oapi.ListPushesRequestObject{}) + require.NoError(t, err) + got, ok := resp.(oapi.ListPushes200JSONResponse) + require.True(t, ok) + require.Empty(t, got) +} + +func TestListPushes_ReturnsAll(t *testing.T) { + t.Parallel() + + fake := &fakePushManager{pushes: []imagepush.Push{ + {ID: "push-2", Status: imagepush.StatusPushed, Layers: 3, Bytes: 1024}, + {ID: "push-1", Status: imagepush.StatusFailed}, + }} + svc := &ApiService{PushManager: fake} + resp, err := svc.ListPushes(context.Background(), oapi.ListPushesRequestObject{}) + require.NoError(t, err) + got, ok := resp.(oapi.ListPushes200JSONResponse) + require.True(t, ok) + require.Len(t, got, 2) + require.Equal(t, "push-2", got[0].Id) + require.NotNil(t, got[0].Layers) + require.Equal(t, 3, *got[0].Layers) + require.NotNil(t, got[0].Bytes) + require.Equal(t, int64(1024), *got[0].Bytes) +} + +// pushErrorCodeOf extracts the Code field from any CreatePush/GetPush error response. +func pushErrorCodeOf(resp any) string { + switch r := resp.(type) { + case oapi.CreatePush400JSONResponse: + return r.Code + case oapi.CreatePush404JSONResponse: + return r.Code + case oapi.CreatePush409JSONResponse: + return r.Code + case oapi.GetPush404JSONResponse: + return r.Code + default: + return "" + } +} diff --git a/cmd/api/config/config.go b/cmd/api/config/config.go index 8a22c060..460cf6d2 100644 --- a/cmd/api/config/config.go +++ b/cmd/api/config/config.go @@ -187,6 +187,7 @@ type LimitsConfig struct { MaxMemoryPerInstance string `koanf:"max_memory_per_instance"` MaxTotalVolumeStorage string `koanf:"max_total_volume_storage"` MaxConcurrentBuilds int `koanf:"max_concurrent_builds"` + MaxConcurrentPushes int `koanf:"max_concurrent_pushes"` MaxOverlaySize string `koanf:"max_overlay_size"` MaxImageStorage float64 `koanf:"max_image_storage"` } @@ -430,6 +431,7 @@ func defaultConfig() *Config { MaxMemoryPerInstance: "32GB", MaxTotalVolumeStorage: "", MaxConcurrentBuilds: 1, + MaxConcurrentPushes: 2, MaxOverlaySize: "100GB", MaxImageStorage: 0.2, }, diff --git a/cmd/api/main.go b/cmd/api/main.go index 98d7be72..8ec068c0 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -119,6 +119,19 @@ type ociCacheGCRunner interface { Run(ctx context.Context) error } +// compositeOCICacheRoots fans the GC's extra-root query out to every source +// that tracks cache blobs outside index.json: the embedded registry's +// BuildKit cache tags and the push manager's in-flight push digests. +type compositeOCICacheRoots []ocicachegc.RootsProvider + +func (c compositeOCICacheRoots) LiveCacheManifestDigests() []string { + out := make([]string, 0) + for _, roots := range c { + out = append(out, roots.LiveCacheManifestDigests()...) + } + return out +} + func configureOCICacheGC(cfg *config.Config, roots ocicachegc.RootsProvider, logger *slog.Logger, meter metric.Meter, tracer trace.Tracer) (ociCacheGCRunner, error) { if cfg == nil || !cfg.Images.OCICacheGC.Enabled { return nil, nil @@ -576,7 +589,7 @@ func run() error { ociGC, err := configureOCICacheGC( app.Config, - app.Registry, + compositeOCICacheRoots{app.Registry, app.PushManager}, logger, otelProvider.MeterFor(loglib.SubsystemImages), otelProvider.TracerFor(loglib.SubsystemImages), diff --git a/cmd/api/wire.go b/cmd/api/wire.go index 95c5b00e..133bf41f 100644 --- a/cmd/api/wire.go +++ b/cmd/api/wire.go @@ -14,6 +14,7 @@ import ( "github.com/kernel/hypeman/lib/builds" "github.com/kernel/hypeman/lib/devices" "github.com/kernel/hypeman/lib/guestmemory" + "github.com/kernel/hypeman/lib/imagepush" "github.com/kernel/hypeman/lib/images" "github.com/kernel/hypeman/lib/ingress" "github.com/kernel/hypeman/lib/instances" @@ -40,6 +41,7 @@ type application struct { BuilderManager builders.Manager IngressManager ingress.Manager BuildManager builds.Manager + PushManager imagepush.Manager ResourceManager *resources.Manager GuestMemoryController guestmemory.Controller AutoStandbyController *autostandby.Controller @@ -65,6 +67,7 @@ func initializeApp() (*application, func(), error) { providers.ProvideBuilderManager, providers.ProvideIngressManager, providers.ProvideBuildManager, + providers.ProvidePushManager, providers.ProvideResourceManager, providers.ProvideGuestMemoryController, providers.ProvideAutoStandbyController, diff --git a/cmd/api/wire_gen.go b/cmd/api/wire_gen.go index 3fa5198e..9eb13c6e 100644 --- a/cmd/api/wire_gen.go +++ b/cmd/api/wire_gen.go @@ -15,6 +15,7 @@ import ( "github.com/kernel/hypeman/lib/builds" "github.com/kernel/hypeman/lib/devices" "github.com/kernel/hypeman/lib/guestmemory" + "github.com/kernel/hypeman/lib/imagepush" "github.com/kernel/hypeman/lib/images" "github.com/kernel/hypeman/lib/ingress" "github.com/kernel/hypeman/lib/instances" @@ -67,6 +68,10 @@ func initializeApp() (*application, func(), error) { if err != nil { return nil, nil, err } + imagepushManager, err := providers.ProvidePushManager(paths, config, manager) + if err != nil { + return nil, nil, err + } resourcesManager, err := providers.ProvideResourceManager(context, config, paths, manager, instancesManager, volumesManager) if err != nil { return nil, nil, err @@ -85,7 +90,7 @@ func initializeApp() (*application, func(), error) { if err != nil { return nil, nil, err } - apiService := api.New(config, manager, instancesManager, volumesManager, buildersManager, networkManager, devicesManager, ingressManager, buildsManager, resourcesManager, controller, autostandbyController, vm_metricsManager) + apiService := api.New(config, manager, instancesManager, volumesManager, buildersManager, networkManager, devicesManager, ingressManager, buildsManager, imagepushManager, resourcesManager, controller, autostandbyController, vm_metricsManager) mainApplication := &application{ Ctx: context, Logger: logger, @@ -99,6 +104,7 @@ func initializeApp() (*application, func(), error) { BuilderManager: buildersManager, IngressManager: ingressManager, BuildManager: buildsManager, + PushManager: imagepushManager, ResourceManager: resourcesManager, GuestMemoryController: controller, AutoStandbyController: autostandbyController, @@ -127,6 +133,7 @@ type application struct { BuilderManager builders.Manager IngressManager ingress.Manager BuildManager builds.Manager + PushManager imagepush.Manager ResourceManager *resources.Manager GuestMemoryController guestmemory.Controller AutoStandbyController *autostandby.Controller diff --git a/config.example.yaml b/config.example.yaml index 4cab6e88..ac908464 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -168,4 +168,5 @@ data_dir: /var/lib/hypeman # max_memory_per_instance: 32GB # max_total_volume_storage: "" # 0 or empty = unlimited # max_concurrent_builds: 1 +# max_concurrent_pushes: 2 # max_overlay_size: 100GB diff --git a/lib/imagepush/imagepush.go b/lib/imagepush/imagepush.go index 3ed18f1d..6450542a 100644 --- a/lib/imagepush/imagepush.go +++ b/lib/imagepush/imagepush.go @@ -8,11 +8,13 @@ package imagepush import ( + "bytes" "context" "crypto/sha256" + "encoding/base64" "encoding/hex" "errors" - "strings" + "fmt" "time" "github.com/google/go-containerregistry/pkg/authn" @@ -24,6 +26,11 @@ const ( StatusPushing = "pushing" StatusPushed = "pushed" StatusFailed = "failed" + + // pushTimeout bounds a single registry export so a wedged registry cannot + // pin a queue slot forever; with a bounded concurrency pool one hung push + // would otherwise block every later push. + pushTimeout = 30 * time.Minute ) var ( @@ -71,7 +78,22 @@ func credFingerprint(c *authn.AuthConfig) string { if !credsPresent(c) { return "" } - sum := sha256.Sum256([]byte(strings.Join([]string{c.Username, c.Password, c.Auth, c.IdentityToken, c.RegistryToken}, "\x00"))) + // Normalize the precomputed base64 "user:pass" Auth shorthand into its + // username/password parts so the same login supplied either way hashes + // identically (AuthConfig.UnmarshalJSON already expands it, but a config + // built in code may not have gone through JSON). + username, password := c.Username, c.Password + if c.Auth != "" { + if decoded, err := base64.StdEncoding.DecodeString(c.Auth); err == nil { + if i := bytes.IndexByte(decoded, ':'); i >= 0 { + username, password = string(decoded[:i]), string(decoded[i+1:]) + } + } + } + // IdentityToken and RegistryToken are distinct auth modes (token/registry- + // scoped) and are kept as-is; they can carry a different identity than the + // basic-auth pair. + sum := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%s\x00%s\x00%s", username, password, c.IdentityToken, c.RegistryToken))) return hex.EncodeToString(sum[:]) } @@ -114,9 +136,11 @@ type Manager interface { // ListPushes returns all pushes, newest first. ListPushes(ctx context.Context) ([]Push, error) // WaitForPush blocks until the push reaches a terminal state (pushed or - // failed) or the context is cancelled. + // failed) or the context is cancelled. The HTTP API currently polls the + // persisted job instead; this remains available to in-process callers. WaitForPush(ctx context.Context, id string) error - // InProgressDigests returns the manifest digests of queued and pushing - // jobs so the OCI cache GC can keep their blobs alive mid-push. - InProgressDigests() []string + // LiveCacheManifestDigests implements ocicachegc.RootsProvider: the + // manifest digests of queued and pushing jobs, so the OCI cache GC keeps + // their blobs alive mid-push. + LiveCacheManifestDigests() []string } diff --git a/lib/imagepush/manager.go b/lib/imagepush/manager.go index 21f4d891..6ab78525 100644 --- a/lib/imagepush/manager.go +++ b/lib/imagepush/manager.go @@ -114,63 +114,55 @@ func (m *manager) CreatePush(ctx context.Context, req PushRequest) (*Push, error // a concurrent request for the same digest+target cannot slip in between // and create a duplicate job. The write is one small fsync'd file; keeping // it under the lock is what lets the dedup path hand back a durable record, - // and it only briefly stalls InProgressDigests — cheap next to the registry - // I/O that dominates a push. - var meta *pushMetadata - for { - m.mu.Lock() - if existing, ok := m.inflight[key]; ok { - // Merge only when the in-flight job runs under the same credentials - // as the request. The manager never stores credential values, so it - // compares fingerprints: a request that borrowed credentials cannot - // merge into an anonymous in-flight push (its auth would be silently - // dropped), an anonymous request cannot merge into a credentialed one - // (it would silently inherit another caller's login), and two - // requests that borrowed different logins cannot merge either — one - // would run under the other caller's auth, and an instance can serve - // more than one principal. Surface the conflict instead so the caller - // can retry once the in-flight job completes or match its credentials. - if existing.credFingerprint != fingerprint { - m.mu.Unlock() - return nil, fmt.Errorf("%w: a push of %s to %s is already in flight with different credentials; retry once it completes or match its credentials", ErrCredentialConflict, img.Digest, dstRef.String()) - } - id := existing.id + // and it only briefly stalls the GC live-digest read — cheap next to the + // registry I/O that dominates a push. + m.mu.Lock() + if existing, ok := m.inflight[key]; ok { + // Merge only when the in-flight job runs under the same credentials + // as the request. The manager never stores credential values, so it + // compares fingerprints: a request that borrowed credentials cannot + // merge into an anonymous in-flight push (its auth would be silently + // dropped), an anonymous request cannot merge into a credentialed one + // (it would silently inherit another caller's login), and two + // requests that borrowed different logins cannot merge either — one + // would run under the other caller's auth, and an instance can serve + // more than one principal. Surface the conflict instead so the caller + // can retry once the in-flight job completes or match its credentials. + if existing.credFingerprint != fingerprint { m.mu.Unlock() - push, err := m.GetPush(ctx, id) - if errors.Is(err, ErrNotFound) { - // The job's terminal record could not be persisted and its - // directory was dropped; the queue completion hook releases the - // inflight entry moments later. Wait for that entry (not just the - // key) to go away, then retry the dedup: a concurrent waiter that - // got here first may already have registered a successor, which - // this retry merges into instead of surfacing a bare ErrNotFound - // from a create call. - if err := m.waitForInflightRelease(ctx, key, id); err != nil { - return nil, err - } - continue - } - return push, err + return nil, fmt.Errorf("%w: a push of %s to %s is already in flight with different credentials; retry once it completes or match its credentials", ErrCredentialConflict, img.Digest, dstRef.String()) } - - meta = &pushMetadata{ - ID: cuid2.Generate(), - Status: StatusQueued, - Image: img.Name, - Digest: img.Digest, - Target: dstRef.String(), - Insecure: req.Insecure, - HadCredentials: credsPresent(req.Credentials), - CreatedAt: time.Now(), - } - if err := writeMetadata(m.paths, meta); err != nil { - m.mu.Unlock() - return nil, fmt.Errorf("write initial metadata: %w", err) + id := existing.id + m.mu.Unlock() + push, err := m.GetPush(ctx, id) + if errors.Is(err, ErrNotFound) { + // The job's terminal record could not be persisted and its + // directory was dropped, so the in-flight entry points at nothing + // readable. This is a rare mid-finalization window; surface a + // clear retryable error rather than orchestrating this caller into + // a successor job that does not yet exist. A retry once the entry + // drops creates a fresh job. + return nil, fmt.Errorf("%w: push job %s is being finalized after a record write failure; retry", ErrNotFound, id) } - m.inflight[key] = inflightPush{id: meta.ID, digest: meta.Digest, credFingerprint: fingerprint} + return push, err + } + + meta := &pushMetadata{ + ID: cuid2.Generate(), + Status: StatusQueued, + Image: img.Name, + Digest: img.Digest, + Target: dstRef.String(), + Insecure: req.Insecure, + HadCredentials: credsPresent(req.Credentials), + CreatedAt: time.Now(), + } + if err := writeMetadata(m.paths, meta); err != nil { m.mu.Unlock() - break + return nil, fmt.Errorf("write initial metadata: %w", err) } + m.inflight[key] = inflightPush{id: meta.ID, digest: meta.Digest, credFingerprint: fingerprint} + m.mu.Unlock() metaCopy := *meta queuePos := m.queue.Enqueue(key, func() { @@ -185,9 +177,13 @@ func (m *manager) CreatePush(ctx context.Context, req PushRequest) (*Push, error } func (m *manager) executePush(ctx context.Context, meta *pushMetadata, provider registrypush.Provider) { - // Contain panics in the job goroutine: record a failed terminal and - // notify waiters instead of leaving the job stuck as pushing. The queue - // slot is released by its own deferred completion. + // Bound each export so a wedged registry cannot pin a queue slot forever. + ctx, cancel := context.WithTimeout(ctx, pushTimeout) + defer cancel() + + // Contain panics in the job goroutine: record a failed terminal instead + // of leaving the job stuck as pushing. The queue slot is released by its + // own deferred completion. defer func() { if r := recover(); r != nil { fmt.Fprintf(os.Stderr, "Warning: push %s to %s panicked: %v\n", meta.ID, meta.Target, r) @@ -196,10 +192,12 @@ func (m *manager) executePush(ctx context.Context, meta *pushMetadata, provider meta.Status = StatusFailed meta.Error = &errorMsg meta.CompletedAt = &now - if err := m.writeTerminal(meta); err != nil { - os.RemoveAll(m.paths.PushDir(meta.ID)) + panicErr := fmt.Errorf("push panicked: %v", r) + if err := m.persistTerminal(meta); err != nil { + m.notify(meta.ID, StatusFailed, err) + } else { + m.notify(meta.ID, StatusFailed, panicErr) } - m.notify(meta.ID, StatusFailed, fmt.Errorf("push panicked: %v", r)) } }() @@ -225,26 +223,32 @@ func (m *manager) executePush(ctx context.Context, meta *pushMetadata, provider } meta.CompletedAt = &now + if err := m.persistTerminal(meta); err != nil { + m.notify(meta.ID, StatusFailed, err) + return + } + if pushErr != nil { + m.notify(meta.ID, StatusFailed, pushErr) + } else { + m.notify(meta.ID, StatusPushed, nil) + } +} + +// persistTerminal writes a terminal status and, if that write fails, drops the +// record so GetPush/ListPushes do not surface a half-written job. Both the +// normal completion path and the panic handler use it so they agree on what +// "couldn't persist" means. +func (m *manager) persistTerminal(meta *pushMetadata) error { if err := m.writeTerminal(meta); err != nil { - // The outcome cannot be recorded: drop the record and report the job - // as failed with the persistence problem, so WaitForPush and GetPush - // agree instead of diverging into success-then-not-found. The actual - // push outcome goes to the log. fmt.Fprintf(os.Stderr, "Warning: push %s to %s finished as %s but the job record could not be persisted: %v\n", meta.ID, meta.Target, strings.ToLower(meta.Status), err) os.RemoveAll(m.paths.PushDir(meta.ID)) persistErr := fmt.Errorf("job record could not be persisted: %w", err) errorMsg := persistErr.Error() meta.Status = StatusFailed meta.Error = &errorMsg - m.notify(meta.ID, StatusFailed, persistErr) - return - } - - if pushErr != nil { - m.notify(meta.ID, StatusFailed, pushErr) - } else { - m.notify(meta.ID, StatusPushed, nil) + return persistErr } + return nil } // writeTerminal persists a terminal status, retrying once. @@ -272,30 +276,6 @@ func (m *manager) releaseInflight(key string) func() { } } -// waitForInflightRelease blocks until the key's torn-down inflight entry is -// dropped — or replaced by a successor job a concurrent create registered -// first, which the caller then merges into by retrying the dedup. Waiting on -// the entry's id rather than the key's absence is what keeps a second waiter -// from parking until the successor finishes and then starting a duplicate -// push. The queue releases the key's active slot before it runs the -// completion hook, so once the torn-down entry is gone a fresh Enqueue for -// the key starts immediately. The caller's context bounds the wait. -func (m *manager) waitForInflightRelease(ctx context.Context, key, tornDownID string) error { - for { - m.mu.Lock() - existing, ok := m.inflight[key] - m.mu.Unlock() - if !ok || existing.id != tornDownID { - return nil - } - select { - case <-ctx.Done(): - return ctx.Err() - case <-time.After(5 * time.Millisecond): - } - } -} - func (m *manager) GetPush(ctx context.Context, id string) (*Push, error) { if err := ctx.Err(); err != nil { return nil, err @@ -305,10 +285,7 @@ func (m *manager) GetPush(ctx context.Context, id string) (*Push, error) { return nil, err } - push := meta.toPush() - if meta.Status == StatusQueued { - push.QueuePosition = m.queue.GetPosition(pushKey(meta.Digest, meta.Target, meta.Insecure)) - } + push := m.toPushWithPosition(meta) return push, nil } @@ -323,17 +300,14 @@ func (m *manager) ListPushes(ctx context.Context) ([]Push, error) { pushes := make([]Push, 0, len(metas)) for _, meta := range metas { - push := meta.toPush() - if meta.Status == StatusQueued { - push.QueuePosition = m.queue.GetPosition(pushKey(meta.Digest, meta.Target, meta.Insecure)) - } - pushes = append(pushes, *push) + pushes = append(pushes, *m.toPushWithPosition(meta)) } return pushes, nil } // WaitForPush blocks until the push reaches a terminal state (pushed or -// failed) or the context is cancelled. +// failed) or the context is cancelled. The HTTP API currently polls the +// persisted job instead; this remains available to in-process callers. func (m *manager) WaitForPush(ctx context.Context, id string) error { push, err := m.GetPush(ctx, id) if err != nil { @@ -384,7 +358,20 @@ func pushError(push *Push) error { return fmt.Errorf("push failed") } -func (m *manager) InProgressDigests() []string { +// toPushWithPosition projects a stored record to its domain form and, for a +// queued job, enriches it with the live pending-queue position. GetPush and +// ListPushes share this so the projection cannot drift between them. +func (m *manager) toPushWithPosition(meta *pushMetadata) *Push { + push := meta.toPush() + if meta.Status == StatusQueued { + push.QueuePosition = m.queue.GetPosition(pushKey(meta.Digest, meta.Target, meta.Insecure)) + } + return push +} + +// LiveCacheManifestDigests implements ocicachegc.RootsProvider so in-flight +// push digests are treated as live alongside the OCI layout index. +func (m *manager) LiveCacheManifestDigests() []string { m.mu.Lock() defer m.mu.Unlock() diff --git a/lib/imagepush/manager_test.go b/lib/imagepush/manager_test.go index d77e9145..630274d3 100644 --- a/lib/imagepush/manager_test.go +++ b/lib/imagepush/manager_test.go @@ -2,6 +2,7 @@ package imagepush import ( "context" + "encoding/base64" "errors" "fmt" "io" @@ -162,17 +163,46 @@ func testManager(t *testing.T, maxConcurrent int, provider registrypush.Provider return mgr, digest } +// waitTerminal polls GetPush until the push reaches a terminal (pushed or +// failed) state and returns it. Tests use the same persisted status surface as +// the HTTP API. +func waitTerminal(t *testing.T, mgr Manager, id string) *Push { + t.Helper() + deadline := time.Now().Add(15 * time.Second) + for { + got, err := mgr.GetPush(context.Background(), id) + if err != nil { + t.Fatalf("GetPush %s: %v", id, err) + } + if got.Status == StatusPushed || got.Status == StatusFailed { + return got + } + if time.Now().After(deadline) { + t.Fatalf("push %s never reached a terminal state (status=%s)", id, got.Status) + } + time.Sleep(5 * time.Millisecond) + } +} + +func waitNoInflight(t *testing.T, mgr Manager) { + t.Helper() + deadline := time.Now().Add(15 * time.Second) + for { + if digests := mgr.LiveCacheManifestDigests(); len(digests) == 0 { + return + } + if time.Now().After(deadline) { + t.Fatalf("in-flight digests never cleared: %v", mgr.LiveCacheManifestDigests()) + } + time.Sleep(5 * time.Millisecond) + } +} + // mustPushed waits for the push to reach a terminal pushed state and returns // it, failing the test otherwise. func mustPushed(t *testing.T, mgr Manager, id string) *Push { t.Helper() - if err := mgr.WaitForPush(context.Background(), id); err != nil { - t.Fatalf("WaitForPush %s: %v", id, err) - } - got, err := mgr.GetPush(context.Background(), id) - if err != nil { - t.Fatalf("GetPush %s: %v", id, err) - } + got := waitTerminal(t, mgr, id) if got.Status != StatusPushed { t.Fatalf("push %s status = %s, want pushed (error: %v)", id, got.Status, got.Error) } @@ -188,6 +218,34 @@ func writePushes(t *testing.T, p *paths.Paths, metas ...*pushMetadata) { } } +func TestWaitForPush(t *testing.T) { + mgr, _ := testManager(t, 2, nil, nil) + host := openRegistry(t) + + push, err := mgr.CreatePush(context.Background(), PushRequest{ + Image: "myapp:v1", + Target: host + "/export/app:v1", + Insecure: true, + }) + if err != nil { + t.Fatalf("CreatePush: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + if err := mgr.WaitForPush(ctx, push.ID); err != nil { + t.Fatalf("WaitForPush: %v", err) + } + + got, err := mgr.GetPush(context.Background(), push.ID) + if err != nil { + t.Fatalf("GetPush: %v", err) + } + if got.Status != StatusPushed { + t.Fatalf("push status = %s, want %s", got.Status, StatusPushed) + } +} + func TestCreatePushEndToEnd(t *testing.T) { mgr, digest := testManager(t, 2, nil, nil) host := openRegistry(t) @@ -226,9 +284,7 @@ func TestCreatePushEndToEnd(t *testing.T) { } // No in-flight digests once done. - if digests := mgr.InProgressDigests(); len(digests) != 0 { - t.Errorf("InProgressDigests = %v, want empty", digests) - } + waitNoInflight(t, mgr) } func TestCreatePushDedupesInFlight(t *testing.T) { @@ -250,14 +306,12 @@ func TestCreatePushDedupesInFlight(t *testing.T) { t.Errorf("duplicate push got new ID %s, want %s", second.ID, first.ID) } - if digests := mgr.InProgressDigests(); len(digests) != 1 || digests[0] != digest { - t.Errorf("InProgressDigests = %v, want [%s]", digests, digest) + if digests := mgr.LiveCacheManifestDigests(); len(digests) != 1 || digests[0] != digest { + t.Errorf("LiveCacheManifestDigests = %v, want [%s]", digests, digest) } close(gate) - if err := mgr.WaitForPush(context.Background(), first.ID); err != nil { - t.Fatalf("WaitForPush: %v", err) - } + mustPushed(t, mgr, first.ID) } func TestCreatePushQueuesBehindConcurrencyLimit(t *testing.T) { @@ -306,12 +360,8 @@ func TestCreatePushQueuesBehindConcurrencyLimit(t *testing.T) { } close(gate) - if err := mgr.WaitForPush(context.Background(), first.ID); err != nil { - t.Fatalf("WaitForPush first: %v", err) - } - if err := mgr.WaitForPush(context.Background(), second.ID); err != nil { - t.Fatalf("WaitForPush second: %v", err) - } + mustPushed(t, mgr, first.ID) + mustPushed(t, mgr, second.ID) } func TestCreatePushRejectsInvalidRequests(t *testing.T) { @@ -377,15 +427,7 @@ func TestCreatePushFailureRecorded(t *testing.T) { t.Fatalf("CreatePush: %v", err) } - err = mgr.WaitForPush(context.Background(), push.ID) - if err == nil { - t.Fatal("WaitForPush should fail for a failed push") - } - - got, err := mgr.GetPush(context.Background(), push.ID) - if err != nil { - t.Fatalf("GetPush: %v", err) - } + got := waitTerminal(t, mgr, push.ID) if got.Status != StatusFailed { t.Errorf("status = %s, want failed", got.Status) } @@ -404,9 +446,7 @@ func TestListPushesNewestFirst(t *testing.T) { if err != nil { t.Fatalf("CreatePush a: %v", err) } - if err := mgr.WaitForPush(context.Background(), first.ID); err != nil { - t.Fatalf("WaitForPush a: %v", err) - } + mustPushed(t, mgr, first.ID) second, err := mgr.CreatePush(context.Background(), PushRequest{ Image: "myapp:v1", Target: host + "/export/b:v1", Insecure: true, @@ -414,9 +454,7 @@ func TestListPushesNewestFirst(t *testing.T) { if err != nil { t.Fatalf("CreatePush b: %v", err) } - if err := mgr.WaitForPush(context.Background(), second.ID); err != nil { - t.Fatalf("WaitForPush b: %v", err) - } + mustPushed(t, mgr, second.ID) pushes, err := mgr.ListPushes(context.Background()) if err != nil { @@ -495,14 +533,12 @@ func TestCreatePushDedupesConcurrently(t *testing.T) { } } - if digests := mgr.InProgressDigests(); len(digests) != 1 || digests[0] != digest { - t.Errorf("InProgressDigests = %v, want [%s]", digests, digest) + if digests := mgr.LiveCacheManifestDigests(); len(digests) != 1 || digests[0] != digest { + t.Errorf("LiveCacheManifestDigests = %v, want [%s]", digests, digest) } close(gate) - if err := mgr.WaitForPush(context.Background(), ids[0]); err != nil { - t.Fatalf("WaitForPush: %v", err) - } + mustPushed(t, mgr, ids[0]) } func TestCreatePushCredentialConflict(t *testing.T) { @@ -539,12 +575,8 @@ func TestCreatePushCredentialConflict(t *testing.T) { close(gateA) close(gateB) - if err := mgr.WaitForPush(context.Background(), seeded.ID); err != nil { - t.Fatalf("WaitForPush seeded: %v", err) - } - if err := mgr.WaitForPush(context.Background(), seeded2.ID); err != nil { - t.Fatalf("WaitForPush seeded 2: %v", err) - } + mustPushed(t, mgr, seeded.ID) + mustPushed(t, mgr, seeded2.ID) // The conflicted requests must not have created duplicate jobs: only the // two seeds exist. @@ -592,131 +624,7 @@ func TestCreatePushCredentialMismatch(t *testing.T) { } close(gate) - if err := mgr.WaitForPush(context.Background(), seeded.ID); err != nil { - t.Fatalf("WaitForPush: %v", err) - } -} - -func TestCreatePushDedupSurvivesTornDownKey(t *testing.T) { - mgr, digest := testManager(t, 1, nil, nil) - host := openRegistry(t) - target := host + "/export/app:v1" - - dstRef, err := name.ParseReference(target, name.Insecure) - if err != nil { - t.Fatalf("ParseReference: %v", err) - } - key := pushKey(digest, dstRef.String(), true) - - // Simulate the persist-failure teardown window: the job's record is gone - // from disk but its inflight entry is still registered, released by the - // queue completion hook moments later. - m := mgr.(*manager) - m.mu.Lock() - m.inflight[key] = inflightPush{id: "ghost", digest: digest} - m.mu.Unlock() - go func() { - time.Sleep(50 * time.Millisecond) - m.mu.Lock() - delete(m.inflight, key) - m.mu.Unlock() - }() - - // The dedup path must wait out the torn-down key and create a fresh job, - // not surface ErrNotFound from a create. - push, err := mgr.CreatePush(context.Background(), PushRequest{Image: "myapp:v1", Target: target, Insecure: true}) - if err != nil { - t.Fatalf("CreatePush: %v", err) - } - if push.ID == "ghost" { - t.Fatal("CreatePush returned the torn-down job") - } - mustPushed(t, mgr, push.ID) -} - -func TestCreatePushDedupWaitersMergeIntoSuccessor(t *testing.T) { - mgr, digest := testManager(t, 1, nil, nil) - host := openRegistry(t) - target := host + "/export/app:v1" - - dstRef, err := name.ParseReference(target, name.Insecure) - if err != nil { - t.Fatalf("ParseReference: %v", err) - } - key := pushKey(digest, dstRef.String(), true) - - // Two concurrent creates racing the same torn-down key: one must create - // the successor job and the other must merge into it — not wait out the - // successor and then start a duplicate push. - m := mgr.(*manager) - m.mu.Lock() - m.inflight[key] = inflightPush{id: "ghost", digest: digest} - m.mu.Unlock() - go func() { - time.Sleep(50 * time.Millisecond) - m.mu.Lock() - delete(m.inflight, key) - m.mu.Unlock() - }() - - ids := make([]string, 2) - errs := make([]error, 2) - var wg sync.WaitGroup - for i := range ids { - wg.Add(1) - go func(i int) { - defer wg.Done() - push, err := mgr.CreatePush(context.Background(), PushRequest{Image: "myapp:v1", Target: target, Insecure: true}) - if push != nil { - ids[i] = push.ID - } - errs[i] = err - }(i) - } - wg.Wait() - - for i := range ids { - if errs[i] != nil { - t.Fatalf("CreatePush #%d: %v", i, errs[i]) - } - } - if ids[0] != ids[1] { - t.Errorf("concurrent creates got IDs %s and %s, want one shared successor job", ids[0], ids[1]) - } - mustPushed(t, mgr, ids[0]) - - pushes, err := mgr.ListPushes(context.Background()) - if err != nil { - t.Fatalf("ListPushes: %v", err) - } - if len(pushes) != 1 { - t.Errorf("len(pushes) = %d, want 1 (no duplicate after the successor)", len(pushes)) - } -} - -func TestWaitForPushCancellation(t *testing.T) { - mgr, _ := testManager(t, 1, nil, nil) - host, gate := gatedRegistry(t) - - push, err := mgr.CreatePush(context.Background(), PushRequest{Image: "myapp:v1", Target: host + "/export/app:v1", Insecure: true}) - if err != nil { - t.Fatalf("CreatePush: %v", err) - } - - ctx, cancel := context.WithCancel(context.Background()) - errCh := make(chan error, 1) - go func() { errCh <- mgr.WaitForPush(ctx, push.ID) }() - cancel() - if err := <-errCh; !errors.Is(err, context.Canceled) { - t.Errorf("WaitForPush err = %v, want context.Canceled", err) - } - - // Let the in-flight job finish so its writes land before the fixture's - // TempDir cleanup. - close(gate) - if err := mgr.WaitForPush(context.Background(), push.ID); err != nil { - t.Fatalf("WaitForPush after cancel: %v", err) - } + mustPushed(t, mgr, seeded.ID) } func TestInProgressDigestsDedupesAcrossTargets(t *testing.T) { @@ -735,8 +643,8 @@ func TestInProgressDigestsDedupesAcrossTargets(t *testing.T) { pushes = append(pushes, push.ID) } - if digests := mgr.InProgressDigests(); len(digests) != 1 || digests[0] != digest { - t.Errorf("InProgressDigests = %v, want [%s]", digests, digest) + if digests := mgr.LiveCacheManifestDigests(); len(digests) != 1 || digests[0] != digest { + t.Errorf("LiveCacheManifestDigests = %v, want [%s]", digests, digest) } // Drain the gated jobs so their writes land before the fixture's TempDir @@ -766,9 +674,7 @@ func TestRecoveryDedupesSameKey(t *testing.T) { t.Fatalf("NewManager: %v", err) } - if err := mgr.WaitForPush(context.Background(), "older"); err != nil { - t.Fatalf("WaitForPush older: %v", err) - } + mustPushed(t, mgr, "older") got, err := mgr.GetPush(context.Background(), "newer") if err != nil { @@ -780,9 +686,9 @@ func TestRecoveryDedupesSameKey(t *testing.T) { if got.Error == nil || !strings.Contains(*got.Error, "duplicate of push job older") { t.Errorf("newer error = %v, want duplicate-of-older explanation", got.Error) } - // WaitForPush on the superseded job surfaces the failure rather than hanging. - if err := mgr.WaitForPush(context.Background(), "newer"); err == nil { - t.Error("WaitForPush on superseded job should fail") + // The superseded job surfaces the failure rather than hanging. + if got := waitTerminal(t, mgr, "newer"); got.Status != StatusFailed { + t.Errorf("superseded job status = %s, want failed", got.Status) } } @@ -802,15 +708,6 @@ func TestSequentialSameKeyPushesAllComplete(t *testing.T) { } } -func TestWaitForPushNotFound(t *testing.T) { - mgr, _ := testManager(t, 1, nil, nil) - - err := mgr.WaitForPush(context.Background(), "missing") - if !errors.Is(err, ErrNotFound) { - t.Errorf("err = %v, want ErrNotFound", err) - } -} - // erroringProvider always fails, proving a push that succeeds used the // request's borrowed credentials instead of the manager default. type erroringProvider struct{} @@ -867,9 +764,7 @@ func TestCredentialsNeverPersisted(t *testing.T) { if err != nil { t.Fatalf("CreatePush: %v", err) } - if err := mgr.WaitForPush(context.Background(), push.ID); err != nil { - t.Fatalf("WaitForPush: %v", err) - } + mustPushed(t, mgr, push.ID) data, err := os.ReadFile(p.PushMetadata(push.ID)) if err != nil { @@ -949,14 +844,12 @@ func TestCreatePushMissingBlobs(t *testing.T) { if err != nil { t.Fatalf("CreatePush: %v", err) } - err = mgr.WaitForPush(context.Background(), push.ID) - if err == nil { - t.Fatal("WaitForPush should fail when cache blobs are missing") + got := waitTerminal(t, mgr, push.ID) + if got.Status != StatusFailed { + t.Fatalf("status = %s, want failed (error: %v)", got.Status, got.Error) } - // Depending on timing the failure is observed via the live event (typed) - // or via persisted metadata (string), so accept both forms. - if !errors.Is(err, ocicache.ErrNotFound) && !strings.Contains(err.Error(), ocicache.ErrNotFound.Error()) { - t.Errorf("err = %v, want ocicache.ErrNotFound", err) + if got.Error == nil || !strings.Contains(*got.Error, ocicache.ErrNotFound.Error()) { + t.Errorf("error = %v, want ocicache.ErrNotFound", got.Error) } } @@ -987,14 +880,12 @@ func TestRecoveryFailsWhenBlobsReclaimed(t *testing.T) { t.Fatalf("NewManager: %v", err) } - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - err = mgr.WaitForPush(ctx, "recovered-missing-blobs") - if err == nil { - t.Fatal("WaitForPush should fail when cache blobs were reclaimed") + got := waitTerminal(t, mgr, "recovered-missing-blobs") + if got.Status != StatusFailed { + t.Fatalf("status = %s, want failed (error: %v)", got.Status, got.Error) } - if !errors.Is(err, ocicache.ErrNotFound) && !strings.Contains(err.Error(), ocicache.ErrNotFound.Error()) { - t.Errorf("err = %v, want ocicache.ErrNotFound", err) + if got.Error == nil || !strings.Contains(*got.Error, ocicache.ErrNotFound.Error()) { + t.Errorf("error = %v, want ocicache.ErrNotFound", got.Error) } } @@ -1017,23 +908,13 @@ func TestExecutePushContainsPanic(t *testing.T) { t.Fatalf("CreatePush: %v", err) } - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - err = mgr.WaitForPush(ctx, push.ID) - if err == nil { - t.Fatal("WaitForPush should fail for a panicked push") - } - if !strings.Contains(err.Error(), "panicked") { - t.Errorf("err = %v, want panic explanation", err) - } - - got, err := mgr.GetPush(context.Background(), push.ID) - if err != nil { - t.Fatalf("GetPush: %v", err) - } + got := waitTerminal(t, mgr, push.ID) if got.Status != StatusFailed { t.Errorf("status = %s, want failed", got.Status) } + if got.Error == nil || !strings.Contains(*got.Error, "panicked") { + t.Errorf("error = %v, want panic explanation", got.Error) + } } func TestRecoveryTreatsInsecureAsDistinctKey(t *testing.T) { @@ -1090,3 +971,25 @@ func TestRecoverySweepsOrphanDirs(t *testing.T) { } mustPushed(t, mgr, "real") } + +func TestCredFingerprintNormalizesAuth(t *testing.T) { + // The same login supplied as Username/Password and as the precomputed + // base64 "user:pass" Auth shorthand must hash identically, so in-flight + // dedup does not report a false credential conflict between the two forms. + basic := &authn.AuthConfig{Username: "pusher", Password: "hunter2"} + shorthand := &authn.AuthConfig{ + Auth: base64.StdEncoding.EncodeToString([]byte("pusher:hunter2")), + } + + basicFp := credFingerprint(basic) + if basicFp == "" { + t.Fatal("basic-auth fingerprint should be non-empty") + } + shorthandFp := credFingerprint(shorthand) + if shorthandFp != basicFp { + t.Errorf("Auth-shorthand fingerprint %q != basic %q", shorthandFp, basicFp) + } + if credFingerprint(nil) != "" || credFingerprint(&authn.AuthConfig{}) != "" { + t.Error("anonymous configs should share the empty fingerprint") + } +} diff --git a/lib/imagepush/storage.go b/lib/imagepush/storage.go index a269f374..e231bf07 100644 --- a/lib/imagepush/storage.go +++ b/lib/imagepush/storage.go @@ -85,6 +85,15 @@ func writeMetadata(p *paths.Paths, meta *pushMetadata) error { return fmt.Errorf("rename metadata: %w", err) } + // Sync the directory so the rename itself is durable: the file is fsync'd + // and renamed above, but without a directory sync a crash right after the + // rename can still lose the directory entry. Best-effort — a directory + // sync failure is not worth failing the write over. + if dir, err := os.Open(dir); err == nil { + _ = dir.Sync() + _ = dir.Close() + } + return nil } diff --git a/lib/oapi/oapi.go b/lib/oapi/oapi.go index 4b689311..05d7482f 100644 --- a/lib/oapi/oapi.go +++ b/lib/oapi/oapi.go @@ -197,6 +197,14 @@ const ( MemoryReclaimResponseHostPressureStatePressure MemoryReclaimResponseHostPressureState = "pressure" ) +// Defines values for PushStatus. +const ( + PushStatusFailed PushStatus = "failed" + PushStatusPushed PushStatus = "pushed" + PushStatusPushing PushStatus = "pushing" + PushStatusQueued PushStatus = "queued" +) + // Defines values for RestartPolicyPolicy. const ( Always RestartPolicyPolicy = "always" @@ -691,6 +699,26 @@ type CreateInstanceRequestNetworkEgressEnforcement struct { // while `http_https_only` rejects direct egress only on TCP ports 80 and 443. 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 *PushCredentials `json:"credentials,omitempty"` + + // Image Hypeman image name to push (tag or digest form) + Image string `json:"image"` + + // Insecure Allow pushing to plain-HTTP registries + Insecure *bool `json:"insecure,omitempty"` + + // Target Full remote reference to push to + Target string `json:"target"` +} + // CreateSnapshotRequest defines model for CreateSnapshotRequest. type CreateSnapshotRequest struct { Compression *SnapshotCompressionConfig `json:"compression,omitempty"` @@ -1351,6 +1379,56 @@ type PathInfo struct { Size *int64 `json:"size,omitempty"` } +// Push defines model for Push. +type Push struct { + // Bytes Total compressed layer bytes pushed (only when status is pushed) + Bytes *int64 `json:"bytes,omitempty"` + CompletedAt *time.Time `json:"completed_at"` + CreatedAt time.Time `json:"created_at"` + + // Digest Cached manifest digest being pushed + Digest string `json:"digest"` + + // Error Error message (only when status is failed) + Error *string `json:"error"` + + // Id Push job identifier + Id string `json:"id"` + + // Image Hypeman image name (normalized ref) + Image string `json:"image"` + + // Layers Number of layers pushed (only when status is pushed) + Layers *int `json:"layers,omitempty"` + + // QueuePosition Position in the push queue (only when status is queued) + QueuePosition *int `json:"queue_position"` + Status PushStatus `json:"status"` + + // Target Remote reference the image is pushed to + 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. +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 *string `json:"registry_token,omitempty"` + + // Username Registry username + Username *string `json:"username,omitempty"` +} + +// PushStatus defines model for PushStatus. +type PushStatus string + // ResourceAllocation defines model for ResourceAllocation. type ResourceAllocation struct { // Cpu vCPUs allocated @@ -1938,6 +2016,9 @@ type StartInstanceJSONRequestBody StartInstanceJSONBody // AttachVolumeJSONRequestBody defines body for AttachVolume for application/json ContentType. type AttachVolumeJSONRequestBody = AttachVolumeRequest +// CreatePushJSONRequestBody defines body for CreatePush for application/json ContentType. +type CreatePushJSONRequestBody = CreatePushRequest + // ReclaimMemoryJSONRequestBody defines body for ReclaimMemory for application/json ContentType. type ReclaimMemoryJSONRequestBody = MemoryReclaimRequest @@ -2187,6 +2268,17 @@ type ClientInterface interface { // WaitForInstanceState request WaitForInstanceState(ctx context.Context, id string, params *WaitForInstanceStateParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // ListPushes request + ListPushes(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // CreatePushWithBody request with any body + CreatePushWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + CreatePush(ctx context.Context, body CreatePushJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetPush request + GetPush(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) + // GetResources request GetResources(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -2947,6 +3039,54 @@ func (c *Client) WaitForInstanceState(ctx context.Context, id string, params *Wa return c.Client.Do(req) } +func (c *Client) ListPushes(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewListPushesRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) CreatePushWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreatePushRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) CreatePush(ctx context.Context, body CreatePushJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreatePushRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetPush(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetPushRequest(c.Server, id) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) GetResources(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewGetResourcesRequest(c.Server) if err != nil { @@ -5099,6 +5239,107 @@ func NewWaitForInstanceStateRequest(server string, id string, params *WaitForIns return req, nil } +// NewListPushesRequest generates requests for ListPushes +func NewListPushesRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/pushes") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewCreatePushRequest calls the generic CreatePush builder with application/json body +func NewCreatePushRequest(server string, body CreatePushJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewCreatePushRequestWithBody(server, "application/json", bodyReader) +} + +// NewCreatePushRequestWithBody generates requests for CreatePush with any type of body +func NewCreatePushRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/pushes") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewGetPushRequest generates requests for GetPush +func NewGetPushRequest(server string, id string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/pushes/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewGetResourcesRequest generates requests for GetResources func NewGetResourcesRequest(server string) (*http.Request, error) { var err error @@ -5836,6 +6077,17 @@ type ClientWithResponsesInterface interface { // WaitForInstanceStateWithResponse request WaitForInstanceStateWithResponse(ctx context.Context, id string, params *WaitForInstanceStateParams, reqEditors ...RequestEditorFn) (*WaitForInstanceStateResponse, error) + // ListPushesWithResponse request + ListPushesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListPushesResponse, error) + + // CreatePushWithBodyWithResponse request with any body + CreatePushWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreatePushResponse, error) + + CreatePushWithResponse(ctx context.Context, body CreatePushJSONRequestBody, reqEditors ...RequestEditorFn) (*CreatePushResponse, error) + + // GetPushWithResponse request + GetPushWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*GetPushResponse, error) + // GetResourcesWithResponse request GetResourcesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetResourcesResponse, error) @@ -7040,6 +7292,82 @@ func (r WaitForInstanceStateResponse) StatusCode() int { return 0 } +type ListPushesResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *[]Push + JSON401 *Error + JSON500 *Error +} + +// Status returns HTTPResponse.Status +func (r ListPushesResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ListPushesResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type CreatePushResponse struct { + Body []byte + HTTPResponse *http.Response + JSON202 *Push + JSON400 *Error + JSON401 *Error + JSON404 *Error + JSON409 *Error + JSON500 *Error +} + +// Status returns HTTPResponse.Status +func (r CreatePushResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r CreatePushResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetPushResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Push + JSON401 *Error + JSON404 *Error + JSON500 *Error +} + +// Status returns HTTPResponse.Status +func (r GetPushResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetPushResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type GetResourcesResponse struct { Body []byte HTTPResponse *http.Response @@ -7836,6 +8164,41 @@ func (c *ClientWithResponses) WaitForInstanceStateWithResponse(ctx context.Conte return ParseWaitForInstanceStateResponse(rsp) } +// ListPushesWithResponse request returning *ListPushesResponse +func (c *ClientWithResponses) ListPushesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListPushesResponse, error) { + rsp, err := c.ListPushes(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseListPushesResponse(rsp) +} + +// CreatePushWithBodyWithResponse request with arbitrary body returning *CreatePushResponse +func (c *ClientWithResponses) CreatePushWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreatePushResponse, error) { + rsp, err := c.CreatePushWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreatePushResponse(rsp) +} + +func (c *ClientWithResponses) CreatePushWithResponse(ctx context.Context, body CreatePushJSONRequestBody, reqEditors ...RequestEditorFn) (*CreatePushResponse, error) { + rsp, err := c.CreatePush(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreatePushResponse(rsp) +} + +// GetPushWithResponse request returning *GetPushResponse +func (c *ClientWithResponses) GetPushWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*GetPushResponse, error) { + rsp, err := c.GetPush(ctx, id, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetPushResponse(rsp) +} + // GetResourcesWithResponse request returning *GetResourcesResponse func (c *ClientWithResponses) GetResourcesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetResourcesResponse, error) { rsp, err := c.GetResources(ctx, reqEditors...) @@ -10091,28 +10454,176 @@ func ParseWaitForInstanceStateResponse(rsp *http.Response) (*WaitForInstanceStat return response, nil } -// ParseGetResourcesResponse parses an HTTP response from a GetResourcesWithResponse call -func ParseGetResourcesResponse(rsp *http.Response) (*GetResourcesResponse, error) { +// ParseListPushesResponse parses an HTTP response from a ListPushesWithResponse call +func ParseListPushesResponse(rsp *http.Response) (*ListPushesResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &GetResourcesResponse{ + response := &ListPushesResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest Resources + var dest []Push if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseCreatePushResponse parses an HTTP response from a CreatePushWithResponse call +func ParseCreatePushResponse(rsp *http.Response) (*CreatePushResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &CreatePushResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202: + var dest Push + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON202 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + 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 == 500: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseGetPushResponse parses an HTTP response from a GetPushWithResponse call +func ParseGetPushResponse(rsp *http.Response) (*GetPushResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetPushResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Push + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseGetResourcesResponse parses an HTTP response from a GetResourcesWithResponse call +func ParseGetResourcesResponse(rsp *http.Response) (*GetResourcesResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetResourcesResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Resources + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err @@ -10709,6 +11220,15 @@ type ServerInterface interface { // Wait for instance to reach a target state // (GET /instances/{id}/wait) WaitForInstanceState(w http.ResponseWriter, r *http.Request, id string, params WaitForInstanceStateParams) + // List pushes + // (GET /pushes) + ListPushes(w http.ResponseWriter, r *http.Request) + // Push an image to a remote registry + // (POST /pushes) + CreatePush(w http.ResponseWriter, r *http.Request) + // Get push details + // (GET /pushes/{id}) + GetPush(w http.ResponseWriter, r *http.Request, id string) // Get host resource capacity and allocations // (GET /resources) GetResources(w http.ResponseWriter, r *http.Request) @@ -11030,6 +11550,24 @@ func (_ Unimplemented) WaitForInstanceState(w http.ResponseWriter, r *http.Reque w.WriteHeader(http.StatusNotImplemented) } +// List pushes +// (GET /pushes) +func (_ Unimplemented) ListPushes(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Push an image to a remote registry +// (POST /pushes) +func (_ Unimplemented) CreatePush(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Get push details +// (GET /pushes/{id}) +func (_ Unimplemented) GetPush(w http.ResponseWriter, r *http.Request, id string) { + w.WriteHeader(http.StatusNotImplemented) +} + // Get host resource capacity and allocations // (GET /resources) func (_ Unimplemented) GetResources(w http.ResponseWriter, r *http.Request) { @@ -12605,6 +13143,77 @@ func (siw *ServerInterfaceWrapper) WaitForInstanceState(w http.ResponseWriter, r handler.ServeHTTP(w, r) } +// ListPushes operation middleware +func (siw *ServerInterfaceWrapper) ListPushes(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ListPushes(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// CreatePush operation middleware +func (siw *ServerInterfaceWrapper) CreatePush(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.CreatePush(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetPush operation middleware +func (siw *ServerInterfaceWrapper) GetPush(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "id" ------------- + var id string + + err = runtime.BindStyledParameterWithOptions("simple", "id", chi.URLParam(r, "id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetPush(w, r, id) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // GetResources operation middleware func (siw *ServerInterfaceWrapper) GetResources(w http.ResponseWriter, r *http.Request) { @@ -13235,6 +13844,15 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/instances/{id}/wait", wrapper.WaitForInstanceState) }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/pushes", wrapper.ListPushes) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/pushes", wrapper.CreatePush) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/pushes/{id}", wrapper.GetPush) + }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/resources", wrapper.GetResources) }) @@ -15348,6 +15966,156 @@ func (response WaitForInstanceState500JSONResponse) VisitWaitForInstanceStateRes return json.NewEncoder(w).Encode(response) } +type ListPushesRequestObject struct { +} + +type ListPushesResponseObject interface { + VisitListPushesResponse(w http.ResponseWriter) error +} + +type ListPushes200JSONResponse []Push + +func (response ListPushes200JSONResponse) VisitListPushesResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type ListPushes401JSONResponse Error + +func (response ListPushes401JSONResponse) VisitListPushesResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type ListPushes500JSONResponse Error + +func (response ListPushes500JSONResponse) VisitListPushesResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type CreatePushRequestObject struct { + Body *CreatePushJSONRequestBody +} + +type CreatePushResponseObject interface { + VisitCreatePushResponse(w http.ResponseWriter) error +} + +type CreatePush202ResponseHeaders struct { + Location string + RetryAfter int32 +} + +type CreatePush202JSONResponse struct { + Body Push + Headers CreatePush202ResponseHeaders +} + +func (response CreatePush202JSONResponse) VisitCreatePushResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Location", fmt.Sprint(response.Headers.Location)) + w.Header().Set("Retry-After", fmt.Sprint(response.Headers.RetryAfter)) + w.WriteHeader(202) + + return json.NewEncoder(w).Encode(response.Body) +} + +type CreatePush400JSONResponse Error + +func (response CreatePush400JSONResponse) VisitCreatePushResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type CreatePush401JSONResponse Error + +func (response CreatePush401JSONResponse) VisitCreatePushResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type CreatePush404JSONResponse Error + +func (response CreatePush404JSONResponse) VisitCreatePushResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type CreatePush409JSONResponse Error + +func (response CreatePush409JSONResponse) VisitCreatePushResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + + return json.NewEncoder(w).Encode(response) +} + +type CreatePush500JSONResponse Error + +func (response CreatePush500JSONResponse) VisitCreatePushResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetPushRequestObject struct { + Id string `json:"id"` +} + +type GetPushResponseObject interface { + VisitGetPushResponse(w http.ResponseWriter) error +} + +type GetPush200JSONResponse Push + +func (response GetPush200JSONResponse) VisitGetPushResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetPush401JSONResponse Error + +func (response GetPush401JSONResponse) VisitGetPushResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetPush404JSONResponse Error + +func (response GetPush404JSONResponse) VisitGetPushResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type GetPush500JSONResponse Error + +func (response GetPush500JSONResponse) VisitGetPushResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type GetResourcesRequestObject struct { } @@ -15938,6 +16706,15 @@ type StrictServerInterface interface { // Wait for instance to reach a target state // (GET /instances/{id}/wait) WaitForInstanceState(ctx context.Context, request WaitForInstanceStateRequestObject) (WaitForInstanceStateResponseObject, error) + // List pushes + // (GET /pushes) + ListPushes(ctx context.Context, request ListPushesRequestObject) (ListPushesResponseObject, error) + // Push an image to a remote registry + // (POST /pushes) + CreatePush(ctx context.Context, request CreatePushRequestObject) (CreatePushResponseObject, error) + // Get push details + // (GET /pushes/{id}) + GetPush(ctx context.Context, request GetPushRequestObject) (GetPushResponseObject, error) // Get host resource capacity and allocations // (GET /resources) GetResources(ctx context.Context, request GetResourcesRequestObject) (GetResourcesResponseObject, error) @@ -17313,6 +18090,87 @@ func (sh *strictHandler) WaitForInstanceState(w http.ResponseWriter, r *http.Req } } +// ListPushes operation middleware +func (sh *strictHandler) ListPushes(w http.ResponseWriter, r *http.Request) { + var request ListPushesRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.ListPushes(ctx, request.(ListPushesRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ListPushes") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(ListPushesResponseObject); ok { + if err := validResponse.VisitListPushesResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// CreatePush operation middleware +func (sh *strictHandler) CreatePush(w http.ResponseWriter, r *http.Request) { + var request CreatePushRequestObject + + var body CreatePushJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.CreatePush(ctx, request.(CreatePushRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "CreatePush") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(CreatePushResponseObject); ok { + if err := validResponse.VisitCreatePushResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetPush operation middleware +func (sh *strictHandler) GetPush(w http.ResponseWriter, r *http.Request, id string) { + var request GetPushRequestObject + + request.Id = id + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetPush(ctx, request.(GetPushRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetPush") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetPushResponseObject); ok { + if err := validResponse.VisitGetPushResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // GetResources operation middleware func (sh *strictHandler) GetResources(w http.ResponseWriter, r *http.Request) { var request GetResourcesRequestObject @@ -17619,332 +18477,349 @@ 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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", - "PdnufPn45f8PAAD///jHnVExvAEA", + "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", + "AQAA///3h2dLttABAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/lib/providers/providers.go b/lib/providers/providers.go index 1b52e3e4..b67feac6 100644 --- a/lib/providers/providers.go +++ b/lib/providers/providers.go @@ -18,6 +18,7 @@ import ( "github.com/kernel/hypeman/lib/hypervisor" "github.com/kernel/hypeman/lib/hypervisor/cloudhypervisor" "github.com/kernel/hypeman/lib/hypervisor/firecracker" + "github.com/kernel/hypeman/lib/imagepush" "github.com/kernel/hypeman/lib/images" "github.com/kernel/hypeman/lib/ingress" "github.com/kernel/hypeman/lib/instances" @@ -245,6 +246,14 @@ func ProvideRegistry(p *paths.Paths, imageManager images.Manager) (*registry.Reg return registry.New(p, imageManager) } +// ProvidePushManager provides the manager for outbound image pushes to remote +// registries. Credentials default to the server's Docker keychain; API callers +// can instead lend per-request credentials, which the manager borrows for the +// duration of a single push without persisting them. +func ProvidePushManager(p *paths.Paths, cfg *config.Config, imageManager images.Manager) (imagepush.Manager, error) { + return imagepush.NewManager(p, imageManager, nil, cfg.Limits.MaxConcurrentPushes) +} + // ProvideResourceManager provides the resource manager for capacity tracking func ProvideResourceManager(ctx context.Context, cfg *config.Config, p *paths.Paths, imageManager images.Manager, instanceManager instances.Manager, volumeManager volumes.Manager) (*resources.Manager, error) { mgr := resources.NewManager(cfg, p) diff --git a/lib/scopes/scopes.go b/lib/scopes/scopes.go index 0240c9c4..84c7c592 100644 --- a/lib/scopes/scopes.go +++ b/lib/scopes/scopes.go @@ -233,6 +233,11 @@ var RouteScopes = map[string]Scope{ "DELETE /images/{name}": ImageDelete, "GET /images/{name}": ImageRead, + // Pushes (outbound image pushes to remote registries) + "POST /pushes": ImageWrite, + "GET /pushes": ImageRead, + "GET /pushes/{id}": ImageRead, + // Ingresses "GET /ingresses": IngressRead, "POST /ingresses": IngressWrite, diff --git a/openapi.yaml b/openapi.yaml index ed158f3e..f90c1fc1 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1797,7 +1797,102 @@ components: status: $ref: "#/components/schemas/BuildStatus" description: New build status (only for type=status) - + + PushStatus: + type: string + enum: [queued, pushing, pushed, failed] + x-enum-varnames: [PushStatusQueued, PushStatusPushing, PushStatusPushed, PushStatusFailed] + + PushCredentials: + type: object + description: | + 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. + properties: + username: + type: string + description: Registry username + password: + type: string + description: Registry password or access token + format: password + registry_token: + type: string + description: Bearer token sent as-is in the Authorization header + format: password + + CreatePushRequest: + type: object + required: + - image + - target + properties: + image: + type: string + description: Hypeman image name to push (tag or digest form) + example: "docker.io/library/alpine:latest" + target: + type: string + description: Full remote reference to push to + example: "123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:v1" + insecure: + type: boolean + description: Allow pushing to plain-HTTP registries + default: false + credentials: + $ref: "#/components/schemas/PushCredentials" + + Push: + type: object + required: + - id + - image + - digest + - target + - status + - created_at + properties: + id: + type: string + description: Push job identifier + image: + type: string + description: Hypeman image name (normalized ref) + digest: + type: string + description: Cached manifest digest being pushed + target: + type: string + description: Remote reference the image is pushed to + status: + $ref: "#/components/schemas/PushStatus" + queue_position: + type: integer + description: Position in the push queue (only when status is queued) + nullable: true + error: + type: string + description: Error message (only when status is failed) + nullable: true + layers: + type: integer + description: Number of layers pushed (only when status is pushed) + bytes: + type: integer + format: int64 + description: Total compressed layer bytes pushed (only when status is pushed) + created_at: + type: string + format: date-time + completed_at: + type: string + format: date-time + nullable: true + Build: type: object required: [id, status, created_at] @@ -4647,3 +4742,134 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + /pushes: + get: + summary: List pushes + description: Lists outbound image push jobs, newest first. + operationId: listPushes + security: + - bearerAuth: [] + responses: + 200: + description: List of pushes + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Push" + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 500: + description: Internal server error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Push an image to a remote registry + description: | + Creates a push job that exports a hypeman image from the local OCI + cache to a remote registry (e.g. AWS ECR, Docker Hub). Credentials are + borrowed from the request when provided and never persisted; otherwise + the server's own registry credentials are used. Only images in the + ready state can be pushed. + operationId: createPush + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreatePushRequest" + responses: + 202: + description: Push job created + headers: + Location: + description: URL of the created push job + schema: + type: string + Retry-After: + description: Minimum number of seconds before polling the push status + schema: + type: integer + format: int32 + content: + application/json: + schema: + $ref: "#/components/schemas/Push" + 400: + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 404: + description: Image not found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 409: + description: Image exists but is not ready + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 500: + description: Internal server error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + /pushes/{id}: + get: + summary: Get push details + operationId: getPush + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + description: Push ID + responses: + 200: + description: Push details + content: + application/json: + schema: + $ref: "#/components/schemas/Push" + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 404: + description: Push not found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 500: + description: Internal server error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" diff --git a/stainless.yaml b/stainless.yaml index aec9abba..3c40590a 100644 --- a/stainless.yaml +++ b/stainless.yaml @@ -223,6 +223,17 @@ resources: cancel: delete /builds/{id} events: get /builds/{id}/events + pushes: + models: + push: "#/components/schemas/Push" + push_status: "#/components/schemas/PushStatus" + push_credentials: "#/components/schemas/PushCredentials" + create_push_request: "#/components/schemas/CreatePushRequest" + methods: + list: get /pushes + create: post /pushes + get: get /pushes/{id} + settings: # All generated integration tests that hit the prism mock http server are marked # as skipped. Removing this setting or setting it to false enables tests, but