Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f3ebd6f
Add imagepush manager for outbound image push jobs
chruffins Aug 5, 2026
b6ac5cc
Support borrowed per-push registry credentials
chruffins Aug 5, 2026
c4a65c4
Fix push job lifecycle races and orphan handling
chruffins Aug 5, 2026
954aaee
Address review: panic-safe queue completion, credentialed orphans, pe…
chruffins Aug 5, 2026
3a74966
Notify subscribers when a recovered push is closed
chruffins Aug 5, 2026
8f4fdd0
Fix remaining review findings on push lifecycle
chruffins Aug 7, 2026
107efc1
imagepush: fix credential-blind dedup and drop dead orphan adoption
chruffins Aug 7, 2026
5f7038f
imagepush: share a generic queue and slim the test harness
chruffins Aug 7, 2026
888c9f3
imagepush: dedupe test wait/assert scaffolding
chruffins Aug 7, 2026
d98dec5
imagepush: address parallel-review findings
chruffins Aug 10, 2026
da865ae
imagepush: deflake queue completion-hook ordering test
chruffins Aug 10, 2026
2c7f833
images: fix build-phase metrics test after build-queue consolidation
chruffins Aug 10, 2026
68b750e
imagepush: address review findings on dedup, recovery, and tests
chruffins Aug 10, 2026
f461185
imagepush: merge dedup waiters into successor instead of re-pushing
chruffins Aug 10, 2026
6edf954
Add pushes API for exporting images to remote registries
chruffins Aug 5, 2026
2076b55
Fix gofmt alignment for push manager wiring
chruffins Aug 5, 2026
b38c3b6
Fix push status enum varnames and empty-credentials fallback
chruffins Aug 5, 2026
a9a4500
Add pushes resource to the Stainless SDK config
chruffins Aug 5, 2026
13a1a28
Address pushes API review: push timeout, drop dead wait surface, simp…
chruffins Aug 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/api/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -62,6 +65,7 @@ func New(
DeviceManager: deviceManager,
IngressManager: ingressManager,
BuildManager: buildManager,
PushManager: pushManager,
ResourceManager: resourceManager,
GuestMemoryController: guestMemoryController,
AutoStandbyController: autoStandbyController,
Expand Down
144 changes: 144 additions & 0 deletions cmd/api/api/pushes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
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"
)

func (s *ApiService) CreatePush(ctx context.Context, request oapi.CreatePushRequestObject) (oapi.CreatePushResponseObject, error) {
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
}
Comment thread
cursor[bot] marked this conversation as resolved.

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.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
}
Comment thread
cursor[bot] marked this conversation as resolved.
}

return oapi.CreatePush202JSONResponse(pushToOAPI(*push)), 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
Comment thread
cursor[bot] marked this conversation as resolved.
}

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 == oapi.PushStatus(imagepush.StatusPushed) {

Check failure on line 137 in cmd/api/api/pushes.go

View workflow job for this annotation

GitHub Actions / test

invalid operation: push.Status == oapi.PushStatus(imagepush.StatusPushed) (mismatched types string and oapi.PushStatus)

Check failure on line 137 in cmd/api/api/pushes.go

View workflow job for this annotation

GitHub Actions / test-darwin

invalid operation: push.Status == oapi.PushStatus(imagepush.StatusPushed) (mismatched types string and oapi.PushStatus)

Check failure on line 137 in cmd/api/api/pushes.go

View workflow job for this annotation

GitHub Actions / test

invalid operation: push.Status == oapi.PushStatus(imagepush.StatusPushed) (mismatched types string and oapi.PushStatus)

Check failure on line 137 in cmd/api/api/pushes.go

View workflow job for this annotation

GitHub Actions / test-darwin

invalid operation: push.Status == oapi.PushStatus(imagepush.StatusPushed) (mismatched types string and oapi.PushStatus)
layers := push.Layers
out.Layers = &layers
bytes := push.Bytes
out.Bytes = &bytes
}
Comment thread
cursor[bot] marked this conversation as resolved.
return out
}
Loading
Loading