daemon split (2/5): extract *ImageService service
Second phase of splitting the daemon god-struct. ImageService now owns
all image + kernel registry operations: register/promote/delete/pull
for images (bundle + OCI paths), the six kernel commands, and the
shared SSH-key/work-seed injection helpers. imageOpsMu (the
publication-window lock) lives on the service; so do the three OCI
pull test seams pullAndFlatten / finalizePulledRootfs / bundleFetch.
The four files images.go, images_pull.go, image_seed.go, kernels.go
flipped their receivers from *Daemon to *ImageService.
FindImage moved with the service. Daemon keeps a thin FindImage
forwarder so callers reading the dispatch code see the obvious
facade and tests that pre-date the split still compile.
flattenNestedWorkHome — called from image_seed.go, vm_authsync.go,
and vm_disk.go across future service boundaries — became a
package-level helper taking a CommandRunner explicitly. Daemon keeps
a deprecated forwarder for now; the other services will use the
package form.
Lazy-init helper imageSvc() on Daemon mirrors hostNet() from
Phase 1, so test literals like &Daemon{store: db, runner: r, ...}
that don't spell out an ImageService still get a working one.
Tests that override the image test seams (autopull_test,
concurrency_test, images_pull_test, images_pull_bundle_test) now
assign d.img = &ImageService{...seams...}; the two-statement pattern
matches what Phase 1 established for HostNetwork.
Dispatch in daemon.go is cleaner now: every image/kernel RPC handler
is a single-liner forwarding to d.imageSvc().*. Phase 5 will do the
same for VM lifecycle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
362009d747
commit
d7614a3b2b
15 changed files with 389 additions and 209 deletions
|
|
@ -19,6 +19,11 @@ func TestFindOrAutoPullImageReturnsLocalWithoutPulling(t *testing.T) {
|
||||||
layout: paths.Layout{ImagesDir: t.TempDir()},
|
layout: paths.Layout{ImagesDir: t.TempDir()},
|
||||||
store: openDaemonStore(t),
|
store: openDaemonStore(t),
|
||||||
runner: system.NewRunner(),
|
runner: system.NewRunner(),
|
||||||
|
}
|
||||||
|
d.img = &ImageService{
|
||||||
|
layout: d.layout,
|
||||||
|
store: d.store,
|
||||||
|
runner: d.runner,
|
||||||
bundleFetch: func(context.Context, string, imagecat.CatEntry) (imagecat.Manifest, error) {
|
bundleFetch: func(context.Context, string, imagecat.CatEntry) (imagecat.Manifest, error) {
|
||||||
t.Fatal("bundleFetch should not be called when image is local")
|
t.Fatal("bundleFetch should not be called when image is local")
|
||||||
return imagecat.Manifest{}, nil
|
return imagecat.Manifest{}, nil
|
||||||
|
|
@ -52,6 +57,11 @@ func TestFindOrAutoPullImagePullsFromCatalog(t *testing.T) {
|
||||||
layout: paths.Layout{ImagesDir: imagesDir, KernelsDir: kernelsDir},
|
layout: paths.Layout{ImagesDir: imagesDir, KernelsDir: kernelsDir},
|
||||||
store: openDaemonStore(t),
|
store: openDaemonStore(t),
|
||||||
runner: system.NewRunner(),
|
runner: system.NewRunner(),
|
||||||
|
}
|
||||||
|
d.img = &ImageService{
|
||||||
|
layout: d.layout,
|
||||||
|
store: d.store,
|
||||||
|
runner: d.runner,
|
||||||
bundleFetch: func(ctx context.Context, destDir string, entry imagecat.CatEntry) (imagecat.Manifest, error) {
|
bundleFetch: func(ctx context.Context, destDir string, entry imagecat.CatEntry) (imagecat.Manifest, error) {
|
||||||
pullCalls++
|
pullCalls++
|
||||||
return stubBundleFetch(imagecat.Manifest{KernelRef: "generic-6.12"})(ctx, destDir, entry)
|
return stubBundleFetch(imagecat.Manifest{KernelRef: "generic-6.12"})(ctx, destDir, entry)
|
||||||
|
|
@ -87,7 +97,7 @@ func TestReadOrAutoPullKernelReturnsLocalWithoutPulling(t *testing.T) {
|
||||||
seedKernel(t, kernelsDir, "generic-6.12")
|
seedKernel(t, kernelsDir, "generic-6.12")
|
||||||
d := &Daemon{layout: paths.Layout{KernelsDir: kernelsDir}}
|
d := &Daemon{layout: paths.Layout{KernelsDir: kernelsDir}}
|
||||||
|
|
||||||
entry, err := d.readOrAutoPullKernel(context.Background(), "generic-6.12")
|
entry, err := d.imageSvc().readOrAutoPullKernel(context.Background(), "generic-6.12")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("readOrAutoPullKernel: %v", err)
|
t.Fatalf("readOrAutoPullKernel: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -98,7 +108,7 @@ func TestReadOrAutoPullKernelReturnsLocalWithoutPulling(t *testing.T) {
|
||||||
|
|
||||||
func TestReadOrAutoPullKernelErrorsWhenNotInCatalog(t *testing.T) {
|
func TestReadOrAutoPullKernelErrorsWhenNotInCatalog(t *testing.T) {
|
||||||
d := &Daemon{layout: paths.Layout{KernelsDir: t.TempDir()}}
|
d := &Daemon{layout: paths.Layout{KernelsDir: t.TempDir()}}
|
||||||
_, err := d.readOrAutoPullKernel(context.Background(), "nonexistent-kernel")
|
_, err := d.imageSvc().readOrAutoPullKernel(context.Background(), "nonexistent-kernel")
|
||||||
if err == nil || !strings.Contains(err.Error(), "not found") {
|
if err == nil || !strings.Contains(err.Error(), "not found") {
|
||||||
t.Fatalf("err = %v, want not-found", err)
|
t.Fatalf("err = %v, want not-found", err)
|
||||||
}
|
}
|
||||||
|
|
@ -120,7 +130,7 @@ func TestReadOrAutoPullKernelSurfacesNonNotExistError(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
d := &Daemon{layout: paths.Layout{KernelsDir: kernelsDir}}
|
d := &Daemon{layout: paths.Layout{KernelsDir: kernelsDir}}
|
||||||
_, err := d.readOrAutoPullKernel(context.Background(), "broken-kernel")
|
_, err := d.imageSvc().readOrAutoPullKernel(context.Background(), "broken-kernel")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("want error")
|
t.Fatal("want error")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,9 +65,14 @@ func TestPullImageDoesNotSerialiseOnDifferentNames(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
d := &Daemon{
|
d := &Daemon{
|
||||||
layout: paths.Layout{ImagesDir: imagesDir, OCICacheDir: cacheDir},
|
layout: paths.Layout{ImagesDir: imagesDir, OCICacheDir: cacheDir},
|
||||||
store: openDaemonStore(t),
|
store: openDaemonStore(t),
|
||||||
runner: system.NewRunner(),
|
runner: system.NewRunner(),
|
||||||
|
}
|
||||||
|
d.img = &ImageService{
|
||||||
|
layout: d.layout,
|
||||||
|
store: d.store,
|
||||||
|
runner: d.runner,
|
||||||
pullAndFlatten: slowPullAndFlatten,
|
pullAndFlatten: slowPullAndFlatten,
|
||||||
finalizePulledRootfs: stubFinalizePulledRootfs,
|
finalizePulledRootfs: stubFinalizePulledRootfs,
|
||||||
}
|
}
|
||||||
|
|
@ -88,7 +93,7 @@ func TestPullImageDoesNotSerialiseOnDifferentNames(t *testing.T) {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(i int, name string) {
|
go func(i int, name string) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
_, err := d.PullImage(context.Background(), mkParams(name))
|
_, err := d.img.PullImage(context.Background(), mkParams(name))
|
||||||
errs[i] = err
|
errs[i] = err
|
||||||
}(i, name)
|
}(i, name)
|
||||||
}
|
}
|
||||||
|
|
@ -146,9 +151,14 @@ func TestPullImageRejectsNameClashAtPublish(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
d := &Daemon{
|
d := &Daemon{
|
||||||
layout: paths.Layout{ImagesDir: imagesDir, OCICacheDir: cacheDir},
|
layout: paths.Layout{ImagesDir: imagesDir, OCICacheDir: cacheDir},
|
||||||
store: openDaemonStore(t),
|
store: openDaemonStore(t),
|
||||||
runner: system.NewRunner(),
|
runner: system.NewRunner(),
|
||||||
|
}
|
||||||
|
d.img = &ImageService{
|
||||||
|
layout: d.layout,
|
||||||
|
store: d.store,
|
||||||
|
runner: d.runner,
|
||||||
pullAndFlatten: pullAndFlatten,
|
pullAndFlatten: pullAndFlatten,
|
||||||
finalizePulledRootfs: stubFinalizePulledRootfs,
|
finalizePulledRootfs: stubFinalizePulledRootfs,
|
||||||
}
|
}
|
||||||
|
|
@ -167,7 +177,7 @@ func TestPullImageRejectsNameClashAtPublish(t *testing.T) {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(i int) {
|
go func(i int) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
_, err := d.PullImage(context.Background(), params)
|
_, err := d.img.PullImage(context.Background(), params)
|
||||||
errs[i] = err
|
errs[i] = err
|
||||||
}(i)
|
}(i)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,6 @@ import (
|
||||||
"banger/internal/config"
|
"banger/internal/config"
|
||||||
"banger/internal/daemon/opstate"
|
"banger/internal/daemon/opstate"
|
||||||
ws "banger/internal/daemon/workspace"
|
ws "banger/internal/daemon/workspace"
|
||||||
"banger/internal/imagecat"
|
|
||||||
"banger/internal/imagepull"
|
|
||||||
"banger/internal/model"
|
"banger/internal/model"
|
||||||
"banger/internal/paths"
|
"banger/internal/paths"
|
||||||
"banger/internal/rpc"
|
"banger/internal/rpc"
|
||||||
|
|
@ -35,7 +33,6 @@ type Daemon struct {
|
||||||
store *store.Store
|
store *store.Store
|
||||||
runner system.CommandRunner
|
runner system.CommandRunner
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
imageOpsMu sync.Mutex
|
|
||||||
createVMMu sync.Mutex
|
createVMMu sync.Mutex
|
||||||
createOps opstate.Registry[*vmCreateOperationState]
|
createOps opstate.Registry[*vmCreateOperationState]
|
||||||
vmLocks vmLockSet
|
vmLocks vmLockSet
|
||||||
|
|
@ -53,14 +50,12 @@ type Daemon struct {
|
||||||
// handles.json scratch file and OS inspection.
|
// handles.json scratch file and OS inspection.
|
||||||
handles *handleCache
|
handles *handleCache
|
||||||
net *HostNetwork
|
net *HostNetwork
|
||||||
|
img *ImageService
|
||||||
closing chan struct{}
|
closing chan struct{}
|
||||||
once sync.Once
|
once sync.Once
|
||||||
pid int
|
pid int
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
vmCaps []vmCapability
|
vmCaps []vmCapability
|
||||||
pullAndFlatten func(ctx context.Context, ref, cacheDir, destDir string) (imagepull.Metadata, error)
|
|
||||||
finalizePulledRootfs func(ctx context.Context, ext4File string, meta imagepull.Metadata) error
|
|
||||||
bundleFetch func(ctx context.Context, destDir string, entry imagecat.CatEntry) (imagecat.Manifest, error)
|
|
||||||
requestHandler func(context.Context, rpc.Request) rpc.Response
|
requestHandler func(context.Context, rpc.Request) rpc.Response
|
||||||
guestWaitForSSH func(context.Context, string, string, time.Duration) error
|
guestWaitForSSH func(context.Context, string, string, time.Duration) error
|
||||||
guestDial func(context.Context, string, string) (guestSSHClient, error)
|
guestDial func(context.Context, string, string) (guestSSHClient, error)
|
||||||
|
|
@ -449,68 +444,68 @@ func (d *Daemon) dispatch(ctx context.Context, req rpc.Request) rpc.Response {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return rpc.NewError("bad_request", err.Error())
|
return rpc.NewError("bad_request", err.Error())
|
||||||
}
|
}
|
||||||
image, err := d.FindImage(ctx, params.IDOrName)
|
image, err := d.imageSvc().FindImage(ctx, params.IDOrName)
|
||||||
return marshalResultOrError(api.ImageShowResult{Image: image}, err)
|
return marshalResultOrError(api.ImageShowResult{Image: image}, err)
|
||||||
case "image.register":
|
case "image.register":
|
||||||
params, err := rpc.DecodeParams[api.ImageRegisterParams](req)
|
params, err := rpc.DecodeParams[api.ImageRegisterParams](req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return rpc.NewError("bad_request", err.Error())
|
return rpc.NewError("bad_request", err.Error())
|
||||||
}
|
}
|
||||||
image, err := d.RegisterImage(ctx, params)
|
image, err := d.imageSvc().RegisterImage(ctx, params)
|
||||||
return marshalResultOrError(api.ImageShowResult{Image: image}, err)
|
return marshalResultOrError(api.ImageShowResult{Image: image}, err)
|
||||||
case "image.promote":
|
case "image.promote":
|
||||||
params, err := rpc.DecodeParams[api.ImageRefParams](req)
|
params, err := rpc.DecodeParams[api.ImageRefParams](req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return rpc.NewError("bad_request", err.Error())
|
return rpc.NewError("bad_request", err.Error())
|
||||||
}
|
}
|
||||||
image, err := d.PromoteImage(ctx, params.IDOrName)
|
image, err := d.imageSvc().PromoteImage(ctx, params.IDOrName)
|
||||||
return marshalResultOrError(api.ImageShowResult{Image: image}, err)
|
return marshalResultOrError(api.ImageShowResult{Image: image}, err)
|
||||||
case "image.delete":
|
case "image.delete":
|
||||||
params, err := rpc.DecodeParams[api.ImageRefParams](req)
|
params, err := rpc.DecodeParams[api.ImageRefParams](req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return rpc.NewError("bad_request", err.Error())
|
return rpc.NewError("bad_request", err.Error())
|
||||||
}
|
}
|
||||||
image, err := d.DeleteImage(ctx, params.IDOrName)
|
image, err := d.imageSvc().DeleteImage(ctx, params.IDOrName)
|
||||||
return marshalResultOrError(api.ImageShowResult{Image: image}, err)
|
return marshalResultOrError(api.ImageShowResult{Image: image}, err)
|
||||||
case "image.pull":
|
case "image.pull":
|
||||||
params, err := rpc.DecodeParams[api.ImagePullParams](req)
|
params, err := rpc.DecodeParams[api.ImagePullParams](req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return rpc.NewError("bad_request", err.Error())
|
return rpc.NewError("bad_request", err.Error())
|
||||||
}
|
}
|
||||||
image, err := d.PullImage(ctx, params)
|
image, err := d.imageSvc().PullImage(ctx, params)
|
||||||
return marshalResultOrError(api.ImageShowResult{Image: image}, err)
|
return marshalResultOrError(api.ImageShowResult{Image: image}, err)
|
||||||
case "kernel.list":
|
case "kernel.list":
|
||||||
return marshalResultOrError(d.KernelList(ctx))
|
return marshalResultOrError(d.imageSvc().KernelList(ctx))
|
||||||
case "kernel.show":
|
case "kernel.show":
|
||||||
params, err := rpc.DecodeParams[api.KernelRefParams](req)
|
params, err := rpc.DecodeParams[api.KernelRefParams](req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return rpc.NewError("bad_request", err.Error())
|
return rpc.NewError("bad_request", err.Error())
|
||||||
}
|
}
|
||||||
entry, err := d.KernelShow(ctx, params.Name)
|
entry, err := d.imageSvc().KernelShow(ctx, params.Name)
|
||||||
return marshalResultOrError(api.KernelShowResult{Entry: entry}, err)
|
return marshalResultOrError(api.KernelShowResult{Entry: entry}, err)
|
||||||
case "kernel.delete":
|
case "kernel.delete":
|
||||||
params, err := rpc.DecodeParams[api.KernelRefParams](req)
|
params, err := rpc.DecodeParams[api.KernelRefParams](req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return rpc.NewError("bad_request", err.Error())
|
return rpc.NewError("bad_request", err.Error())
|
||||||
}
|
}
|
||||||
err = d.KernelDelete(ctx, params.Name)
|
err = d.imageSvc().KernelDelete(ctx, params.Name)
|
||||||
return marshalResultOrError(api.Empty{}, err)
|
return marshalResultOrError(api.Empty{}, err)
|
||||||
case "kernel.import":
|
case "kernel.import":
|
||||||
params, err := rpc.DecodeParams[api.KernelImportParams](req)
|
params, err := rpc.DecodeParams[api.KernelImportParams](req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return rpc.NewError("bad_request", err.Error())
|
return rpc.NewError("bad_request", err.Error())
|
||||||
}
|
}
|
||||||
entry, err := d.KernelImport(ctx, params)
|
entry, err := d.imageSvc().KernelImport(ctx, params)
|
||||||
return marshalResultOrError(api.KernelShowResult{Entry: entry}, err)
|
return marshalResultOrError(api.KernelShowResult{Entry: entry}, err)
|
||||||
case "kernel.pull":
|
case "kernel.pull":
|
||||||
params, err := rpc.DecodeParams[api.KernelPullParams](req)
|
params, err := rpc.DecodeParams[api.KernelPullParams](req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return rpc.NewError("bad_request", err.Error())
|
return rpc.NewError("bad_request", err.Error())
|
||||||
}
|
}
|
||||||
entry, err := d.KernelPull(ctx, params)
|
entry, err := d.imageSvc().KernelPull(ctx, params)
|
||||||
return marshalResultOrError(api.KernelShowResult{Entry: entry}, err)
|
return marshalResultOrError(api.KernelShowResult{Entry: entry}, err)
|
||||||
case "kernel.catalog":
|
case "kernel.catalog":
|
||||||
return marshalResultOrError(d.KernelCatalog(ctx))
|
return marshalResultOrError(d.imageSvc().KernelCatalog(ctx))
|
||||||
default:
|
default:
|
||||||
return rpc.NewError("unknown_method", req.Method)
|
return rpc.NewError("unknown_method", req.Method)
|
||||||
}
|
}
|
||||||
|
|
@ -619,35 +614,11 @@ func (d *Daemon) FindVM(ctx context.Context, idOrName string) (model.VMRecord, e
|
||||||
return model.VMRecord{}, fmt.Errorf("vm %q not found", idOrName)
|
return model.VMRecord{}, fmt.Errorf("vm %q not found", idOrName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindImage stays on Daemon as a thin forwarder to the image service
|
||||||
|
// lookup so callers reading dispatch code see the obvious facade, and
|
||||||
|
// tests that pre-date the service split still compile.
|
||||||
func (d *Daemon) FindImage(ctx context.Context, idOrName string) (model.Image, error) {
|
func (d *Daemon) FindImage(ctx context.Context, idOrName string) (model.Image, error) {
|
||||||
if idOrName == "" {
|
return d.imageSvc().FindImage(ctx, idOrName)
|
||||||
return model.Image{}, errors.New("image id or name is required")
|
|
||||||
}
|
|
||||||
if image, err := d.store.GetImageByName(ctx, idOrName); err == nil {
|
|
||||||
return image, nil
|
|
||||||
}
|
|
||||||
if image, err := d.store.GetImageByID(ctx, idOrName); err == nil {
|
|
||||||
return image, nil
|
|
||||||
}
|
|
||||||
images, err := d.store.ListImages(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return model.Image{}, err
|
|
||||||
}
|
|
||||||
matchCount := 0
|
|
||||||
var match model.Image
|
|
||||||
for _, image := range images {
|
|
||||||
if strings.HasPrefix(image.ID, idOrName) || strings.HasPrefix(image.Name, idOrName) {
|
|
||||||
match = image
|
|
||||||
matchCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if matchCount == 1 {
|
|
||||||
return match, nil
|
|
||||||
}
|
|
||||||
if matchCount > 1 {
|
|
||||||
return model.Image{}, fmt.Errorf("multiple images match %q", idOrName)
|
|
||||||
}
|
|
||||||
return model.Image{}, fmt.Errorf("image %q not found", idOrName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Daemon) TouchVM(ctx context.Context, idOrName string) (model.VMRecord, error) {
|
func (d *Daemon) TouchVM(ctx context.Context, idOrName string) (model.VMRecord, error) {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ func TestRegisterImageRequiresKernel(t *testing.T) {
|
||||||
}
|
}
|
||||||
d := &Daemon{store: openDaemonStore(t)}
|
d := &Daemon{store: openDaemonStore(t)}
|
||||||
|
|
||||||
_, err := d.RegisterImage(context.Background(), api.ImageRegisterParams{
|
_, err := d.imageSvc().RegisterImage(context.Background(), api.ImageRegisterParams{
|
||||||
Name: "missing-kernel",
|
Name: "missing-kernel",
|
||||||
RootfsPath: rootfs,
|
RootfsPath: rootfs,
|
||||||
})
|
})
|
||||||
|
|
@ -100,7 +100,7 @@ func TestPromoteImageCopiesBootArtifactsIntoArtifactDir(t *testing.T) {
|
||||||
store: db,
|
store: db,
|
||||||
runner: system.NewRunner(),
|
runner: system.NewRunner(),
|
||||||
}
|
}
|
||||||
got, err := d.PromoteImage(context.Background(), image.Name)
|
got, err := d.imageSvc().PromoteImage(context.Background(), image.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("PromoteImage: %v", err)
|
t.Fatalf("PromoteImage: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,48 +12,48 @@ import (
|
||||||
"banger/internal/system"
|
"banger/internal/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *Daemon) seedAuthorizedKeyOnExt4Image(ctx context.Context, imagePath string) (string, error) {
|
func (s *ImageService) seedAuthorizedKeyOnExt4Image(ctx context.Context, imagePath string) (string, error) {
|
||||||
if strings.TrimSpace(d.config.SSHKeyPath) == "" {
|
if strings.TrimSpace(s.config.SSHKeyPath) == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
fingerprint, err := guest.AuthorizedPublicKeyFingerprint(d.config.SSHKeyPath)
|
fingerprint, err := guest.AuthorizedPublicKeyFingerprint(s.config.SSHKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("derive authorized ssh key fingerprint: %w", err)
|
return "", fmt.Errorf("derive authorized ssh key fingerprint: %w", err)
|
||||||
}
|
}
|
||||||
publicKey, err := guest.AuthorizedPublicKey(d.config.SSHKeyPath)
|
publicKey, err := guest.AuthorizedPublicKey(s.config.SSHKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("derive authorized ssh key: %w", err)
|
return "", fmt.Errorf("derive authorized ssh key: %w", err)
|
||||||
}
|
}
|
||||||
mountDir, cleanup, err := system.MountTempDir(ctx, d.runner, imagePath, false)
|
mountDir, cleanup, err := system.MountTempDir(ctx, s.runner, imagePath, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
if err := d.flattenNestedWorkHome(ctx, mountDir); err != nil {
|
if err := flattenNestedWorkHome(ctx, s.runner, mountDir); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Same rationale as in ensureAuthorizedKeyOnWorkDisk — the seed's
|
// Same rationale as in ensureAuthorizedKeyOnWorkDisk — the seed's
|
||||||
// filesystem root becomes /root inside the guest, and sshd's
|
// filesystem root becomes /root inside the guest, and sshd's
|
||||||
// StrictModes check walks its ownership and mode.
|
// StrictModes check walks its ownership and mode.
|
||||||
if err := normaliseHomeDirPerms(ctx, d.runner, mountDir); err != nil {
|
if err := normaliseHomeDirPerms(ctx, s.runner, mountDir); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
sshDir := filepath.Join(mountDir, ".ssh")
|
sshDir := filepath.Join(mountDir, ".ssh")
|
||||||
if _, err := d.runner.RunSudo(ctx, "mkdir", "-p", sshDir); err != nil {
|
if _, err := s.runner.RunSudo(ctx, "mkdir", "-p", sshDir); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if _, err := d.runner.RunSudo(ctx, "chmod", "700", sshDir); err != nil {
|
if _, err := s.runner.RunSudo(ctx, "chmod", "700", sshDir); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if _, err := d.runner.RunSudo(ctx, "chown", "0:0", sshDir); err != nil {
|
if _, err := s.runner.RunSudo(ctx, "chown", "0:0", sshDir); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
authorizedKeysPath := filepath.Join(sshDir, "authorized_keys")
|
authorizedKeysPath := filepath.Join(sshDir, "authorized_keys")
|
||||||
existing, err := d.runner.RunSudo(ctx, "cat", authorizedKeysPath)
|
existing, err := s.runner.RunSudo(ctx, "cat", authorizedKeysPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
existing = nil
|
existing = nil
|
||||||
}
|
}
|
||||||
|
|
@ -73,17 +73,17 @@ func (d *Daemon) seedAuthorizedKeyOnExt4Image(ctx context.Context, imagePath str
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer os.Remove(tmpPath)
|
defer os.Remove(tmpPath)
|
||||||
if _, err := d.runner.RunSudo(ctx, "install", "-m", "600", tmpPath, authorizedKeysPath); err != nil {
|
if _, err := s.runner.RunSudo(ctx, "install", "-m", "600", tmpPath, authorizedKeysPath); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return fingerprint, nil
|
return fingerprint, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Daemon) refreshManagedWorkSeedFingerprint(ctx context.Context, image model.Image, fingerprint string) error {
|
func (s *ImageService) refreshManagedWorkSeedFingerprint(ctx context.Context, image model.Image, fingerprint string) error {
|
||||||
if !image.Managed || strings.TrimSpace(image.WorkSeedPath) == "" || strings.TrimSpace(fingerprint) == "" {
|
if !image.Managed || strings.TrimSpace(image.WorkSeedPath) == "" || strings.TrimSpace(fingerprint) == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
seededFingerprint, err := d.seedAuthorizedKeyOnExt4Image(ctx, image.WorkSeedPath)
|
seededFingerprint, err := s.seedAuthorizedKeyOnExt4Image(ctx, image.WorkSeedPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -92,5 +92,5 @@ func (d *Daemon) refreshManagedWorkSeedFingerprint(ctx context.Context, image mo
|
||||||
}
|
}
|
||||||
image.SeededSSHPublicKeyFingerprint = seededFingerprint
|
image.SeededSSHPublicKeyFingerprint = seededFingerprint
|
||||||
image.UpdatedAt = model.Now()
|
image.UpdatedAt = model.Now()
|
||||||
return d.store.UpsertImage(ctx, image)
|
return s.store.UpsertImage(ctx, image)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
129
internal/daemon/image_service.go
Normal file
129
internal/daemon/image_service.go
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"banger/internal/imagecat"
|
||||||
|
"banger/internal/imagepull"
|
||||||
|
"banger/internal/model"
|
||||||
|
"banger/internal/paths"
|
||||||
|
"banger/internal/store"
|
||||||
|
"banger/internal/system"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ImageService owns everything image-registry-related: register /
|
||||||
|
// promote / delete / pull (bundle + OCI), plus the kernel catalog
|
||||||
|
// operations that share the same lifecycle primitives. The publication
|
||||||
|
// lock imageOpsMu lives here so its scope is obvious at the field
|
||||||
|
// definition, and the three OCI-pull test seams (pullAndFlatten,
|
||||||
|
// finalizePulledRootfs, bundleFetch) are fields on the service rather
|
||||||
|
// than mutable globals on Daemon.
|
||||||
|
//
|
||||||
|
// Kept unexported except where peer services (VMService) need it, and
|
||||||
|
// peer access goes through consumer-defined interfaces, not direct
|
||||||
|
// struct poking.
|
||||||
|
type ImageService struct {
|
||||||
|
runner system.CommandRunner
|
||||||
|
logger *slog.Logger
|
||||||
|
config model.DaemonConfig
|
||||||
|
layout paths.Layout
|
||||||
|
store *store.Store
|
||||||
|
|
||||||
|
// imageOpsMu is the publication-window lock: held only across the
|
||||||
|
// "recheck name free + atomic rename + UpsertImage" commit. See
|
||||||
|
// internal/daemon/ARCHITECTURE.md.
|
||||||
|
imageOpsMu sync.Mutex
|
||||||
|
|
||||||
|
// Test seams; nil → real implementation.
|
||||||
|
pullAndFlatten func(ctx context.Context, ref, cacheDir, destDir string) (imagepull.Metadata, error)
|
||||||
|
finalizePulledRootfs func(ctx context.Context, ext4File string, meta imagepull.Metadata) error
|
||||||
|
bundleFetch func(ctx context.Context, destDir string, entry imagecat.CatEntry) (imagecat.Manifest, error)
|
||||||
|
|
||||||
|
// beginOperation is a test seam used by a couple of image ops that
|
||||||
|
// want structured operation logging. Nil → Daemon's beginOperation,
|
||||||
|
// injected at construction.
|
||||||
|
beginOperation func(name string, attrs ...any) *operationLog
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageServiceDeps names every handle ImageService needs from the
|
||||||
|
// Daemon composition root. Using a struct (rather than positional args)
|
||||||
|
// makes the wiring site in Daemon.Open read as a declaration.
|
||||||
|
type imageServiceDeps struct {
|
||||||
|
runner system.CommandRunner
|
||||||
|
logger *slog.Logger
|
||||||
|
config model.DaemonConfig
|
||||||
|
layout paths.Layout
|
||||||
|
store *store.Store
|
||||||
|
beginOperation func(name string, attrs ...any) *operationLog
|
||||||
|
}
|
||||||
|
|
||||||
|
func newImageService(deps imageServiceDeps) *ImageService {
|
||||||
|
return &ImageService{
|
||||||
|
runner: deps.runner,
|
||||||
|
logger: deps.logger,
|
||||||
|
config: deps.config,
|
||||||
|
layout: deps.layout,
|
||||||
|
store: deps.store,
|
||||||
|
beginOperation: deps.beginOperation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindImage is the service-owned lookup helper. It falls back from
|
||||||
|
// exact-name → exact-id → prefix match, matching the historical
|
||||||
|
// daemon.FindImage behaviour. Kept on ImageService because image
|
||||||
|
// lookup is inherently a service concern.
|
||||||
|
func (s *ImageService) FindImage(ctx context.Context, idOrName string) (model.Image, error) {
|
||||||
|
if idOrName == "" {
|
||||||
|
return model.Image{}, fmt.Errorf("image id or name is required")
|
||||||
|
}
|
||||||
|
if image, err := s.store.GetImageByName(ctx, idOrName); err == nil {
|
||||||
|
return image, nil
|
||||||
|
}
|
||||||
|
if image, err := s.store.GetImageByID(ctx, idOrName); err == nil {
|
||||||
|
return image, nil
|
||||||
|
}
|
||||||
|
images, err := s.store.ListImages(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return model.Image{}, err
|
||||||
|
}
|
||||||
|
matchCount := 0
|
||||||
|
var match model.Image
|
||||||
|
for _, image := range images {
|
||||||
|
if strings.HasPrefix(image.ID, idOrName) || strings.HasPrefix(image.Name, idOrName) {
|
||||||
|
match = image
|
||||||
|
matchCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matchCount == 1 {
|
||||||
|
return match, nil
|
||||||
|
}
|
||||||
|
if matchCount > 1 {
|
||||||
|
return model.Image{}, fmt.Errorf("multiple images match %q", idOrName)
|
||||||
|
}
|
||||||
|
return model.Image{}, fmt.Errorf("image %q not found", idOrName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageSvc is the Daemon-side getter that lazy-inits ImageService from
|
||||||
|
// current Daemon fields. Mirrors hostNet() so test literals can keep
|
||||||
|
// using `&Daemon{store: db, runner: r, ...}` and still end up with a
|
||||||
|
// working ImageService.
|
||||||
|
func (d *Daemon) imageSvc() *ImageService {
|
||||||
|
if d.img != nil {
|
||||||
|
return d.img
|
||||||
|
}
|
||||||
|
d.img = newImageService(imageServiceDeps{
|
||||||
|
runner: d.runner,
|
||||||
|
logger: d.logger,
|
||||||
|
config: d.config,
|
||||||
|
layout: d.layout,
|
||||||
|
store: d.store,
|
||||||
|
beginOperation: func(name string, attrs ...any) *operationLog {
|
||||||
|
return d.beginOperation(name, attrs...)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return d.img
|
||||||
|
}
|
||||||
|
|
@ -20,7 +20,7 @@ import (
|
||||||
// validation + kernel resolution run without imageOpsMu — only the
|
// validation + kernel resolution run without imageOpsMu — only the
|
||||||
// lookup-then-upsert atom is held under the lock so concurrent
|
// lookup-then-upsert atom is held under the lock so concurrent
|
||||||
// registers of the same name don't race.
|
// registers of the same name don't race.
|
||||||
func (d *Daemon) RegisterImage(ctx context.Context, params api.ImageRegisterParams) (image model.Image, err error) {
|
func (s *ImageService) RegisterImage(ctx context.Context, params api.ImageRegisterParams) (image model.Image, err error) {
|
||||||
name := strings.TrimSpace(params.Name)
|
name := strings.TrimSpace(params.Name)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return model.Image{}, fmt.Errorf("image name is required")
|
return model.Image{}, fmt.Errorf("image name is required")
|
||||||
|
|
@ -39,7 +39,7 @@ func (d *Daemon) RegisterImage(ctx context.Context, params api.ImageRegisterPara
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
kernelPath, initrdPath, modulesDir, err := d.resolveKernelInputs(ctx, params.KernelRef, params.KernelPath, params.InitrdPath, params.ModulesDir)
|
kernelPath, initrdPath, modulesDir, err := s.resolveKernelInputs(ctx, params.KernelRef, params.KernelPath, params.InitrdPath, params.ModulesDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -48,11 +48,11 @@ func (d *Daemon) RegisterImage(ctx context.Context, params api.ImageRegisterPara
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
d.imageOpsMu.Lock()
|
s.imageOpsMu.Lock()
|
||||||
defer d.imageOpsMu.Unlock()
|
defer s.imageOpsMu.Unlock()
|
||||||
|
|
||||||
now := model.Now()
|
now := model.Now()
|
||||||
existing, lookupErr := d.store.GetImageByName(ctx, name)
|
existing, lookupErr := s.store.GetImageByName(ctx, name)
|
||||||
switch {
|
switch {
|
||||||
case lookupErr == nil:
|
case lookupErr == nil:
|
||||||
if existing.Managed {
|
if existing.Managed {
|
||||||
|
|
@ -88,7 +88,7 @@ func (d *Daemon) RegisterImage(ctx context.Context, params api.ImageRegisterPara
|
||||||
return model.Image{}, lookupErr
|
return model.Image{}, lookupErr
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := d.store.UpsertImage(ctx, image); err != nil {
|
if err := s.store.UpsertImage(ctx, image); err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
return image, nil
|
return image, nil
|
||||||
|
|
@ -99,8 +99,8 @@ func (d *Daemon) RegisterImage(ctx context.Context, params api.ImageRegisterPara
|
||||||
// SSH-key seeding, and boot-artifact staging all happen outside
|
// SSH-key seeding, and boot-artifact staging all happen outside
|
||||||
// imageOpsMu — only the find/rename/upsert commit atom holds the
|
// imageOpsMu — only the find/rename/upsert commit atom holds the
|
||||||
// lock.
|
// lock.
|
||||||
func (d *Daemon) PromoteImage(ctx context.Context, idOrName string) (image model.Image, err error) {
|
func (s *ImageService) PromoteImage(ctx context.Context, idOrName string) (image model.Image, err error) {
|
||||||
op := d.beginOperation("image.promote")
|
op := s.beginOperation("image.promote")
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
op.fail(err, imageLogAttrs(image)...)
|
op.fail(err, imageLogAttrs(image)...)
|
||||||
|
|
@ -109,7 +109,7 @@ func (d *Daemon) PromoteImage(ctx context.Context, idOrName string) (image model
|
||||||
op.done(imageLogAttrs(image)...)
|
op.done(imageLogAttrs(image)...)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
image, err = d.FindImage(ctx, idOrName)
|
image, err = s.FindImage(ctx, idOrName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -119,21 +119,21 @@ func (d *Daemon) PromoteImage(ctx context.Context, idOrName string) (image model
|
||||||
if err := imagemgr.ValidatePromotePaths(image.RootfsPath, image.KernelPath, image.InitrdPath, image.ModulesDir); err != nil {
|
if err := imagemgr.ValidatePromotePaths(image.RootfsPath, image.KernelPath, image.InitrdPath, image.ModulesDir); err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(d.layout.ImagesDir) == "" {
|
if strings.TrimSpace(s.layout.ImagesDir) == "" {
|
||||||
return model.Image{}, errors.New("images dir is not configured")
|
return model.Image{}, errors.New("images dir is not configured")
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(d.layout.ImagesDir, 0o755); err != nil {
|
if err := os.MkdirAll(s.layout.ImagesDir, 0o755); err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
artifactDir := filepath.Join(d.layout.ImagesDir, image.ID)
|
artifactDir := filepath.Join(s.layout.ImagesDir, image.ID)
|
||||||
if _, statErr := os.Stat(artifactDir); statErr == nil {
|
if _, statErr := os.Stat(artifactDir); statErr == nil {
|
||||||
return model.Image{}, fmt.Errorf("artifact dir already exists: %s", artifactDir)
|
return model.Image{}, fmt.Errorf("artifact dir already exists: %s", artifactDir)
|
||||||
} else if !os.IsNotExist(statErr) {
|
} else if !os.IsNotExist(statErr) {
|
||||||
return model.Image{}, statErr
|
return model.Image{}, statErr
|
||||||
}
|
}
|
||||||
|
|
||||||
stageDir, err := os.MkdirTemp(d.layout.ImagesDir, image.ID+".promote-")
|
stageDir, err := os.MkdirTemp(s.layout.ImagesDir, image.ID+".promote-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -167,14 +167,14 @@ func (d *Daemon) PromoteImage(ctx context.Context, idOrName string) (image model
|
||||||
if err := system.CopyFilePreferClone(image.WorkSeedPath, workSeedPath); err != nil {
|
if err := system.CopyFilePreferClone(image.WorkSeedPath, workSeedPath); err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
image.SeededSSHPublicKeyFingerprint, err = d.seedAuthorizedKeyOnExt4Image(ctx, workSeedPath)
|
image.SeededSSHPublicKeyFingerprint, err = s.seedAuthorizedKeyOnExt4Image(ctx, workSeedPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
image.SeededSSHPublicKeyFingerprint = ""
|
image.SeededSSHPublicKeyFingerprint = ""
|
||||||
}
|
}
|
||||||
_, initrdPath, modulesDir, err := imagemgr.StageBootArtifacts(ctx, d.runner, stageDir, image.KernelPath, image.InitrdPath, image.ModulesDir)
|
_, initrdPath, modulesDir, err := imagemgr.StageBootArtifacts(ctx, s.runner, stageDir, image.KernelPath, image.InitrdPath, image.ModulesDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -191,13 +191,13 @@ func (d *Daemon) PromoteImage(ctx context.Context, idOrName string) (image model
|
||||||
image.UpdatedAt = model.Now()
|
image.UpdatedAt = model.Now()
|
||||||
|
|
||||||
op.stage("activate_artifacts", "artifact_dir", artifactDir)
|
op.stage("activate_artifacts", "artifact_dir", artifactDir)
|
||||||
d.imageOpsMu.Lock()
|
s.imageOpsMu.Lock()
|
||||||
defer d.imageOpsMu.Unlock()
|
defer s.imageOpsMu.Unlock()
|
||||||
if err := os.Rename(stageDir, artifactDir); err != nil {
|
if err := os.Rename(stageDir, artifactDir); err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
cleanupStage = false
|
cleanupStage = false
|
||||||
if err := d.store.UpsertImage(ctx, image); err != nil {
|
if err := s.store.UpsertImage(ctx, image); err != nil {
|
||||||
_ = os.RemoveAll(artifactDir)
|
_ = os.RemoveAll(artifactDir)
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -208,22 +208,22 @@ func (d *Daemon) PromoteImage(ctx context.Context, idOrName string) (image model
|
||||||
// imageOpsMu so a concurrent CreateVM can't slip an image_id reference
|
// imageOpsMu so a concurrent CreateVM can't slip an image_id reference
|
||||||
// in between the check and the delete. File cleanup happens after the
|
// in between the check and the delete. File cleanup happens after the
|
||||||
// lock is released — the store row is the authoritative handle.
|
// lock is released — the store row is the authoritative handle.
|
||||||
func (d *Daemon) DeleteImage(ctx context.Context, idOrName string) (model.Image, error) {
|
func (s *ImageService) DeleteImage(ctx context.Context, idOrName string) (model.Image, error) {
|
||||||
image, err := func() (model.Image, error) {
|
image, err := func() (model.Image, error) {
|
||||||
d.imageOpsMu.Lock()
|
s.imageOpsMu.Lock()
|
||||||
defer d.imageOpsMu.Unlock()
|
defer s.imageOpsMu.Unlock()
|
||||||
img, err := d.FindImage(ctx, idOrName)
|
img, err := s.FindImage(ctx, idOrName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
vms, err := d.store.FindVMsUsingImage(ctx, img.ID)
|
vms, err := s.store.FindVMsUsingImage(ctx, img.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
if len(vms) > 0 {
|
if len(vms) > 0 {
|
||||||
return model.Image{}, fmt.Errorf("image %s is still referenced by %d VM(s)", img.Name, len(vms))
|
return model.Image{}, fmt.Errorf("image %s is still referenced by %d VM(s)", img.Name, len(vms))
|
||||||
}
|
}
|
||||||
if err := d.store.DeleteImage(ctx, img.ID); err != nil {
|
if err := s.store.DeleteImage(ctx, img.ID); err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
return img, nil
|
return img, nil
|
||||||
|
|
@ -253,7 +253,7 @@ func firstNonEmpty(values ...string) string {
|
||||||
// When kernelRef is given but not yet pulled locally, an auto-pull from the
|
// When kernelRef is given but not yet pulled locally, an auto-pull from the
|
||||||
// embedded kernelcat catalog fires so the caller doesn't have to manage
|
// embedded kernelcat catalog fires so the caller doesn't have to manage
|
||||||
// kernel/image ordering by hand.
|
// kernel/image ordering by hand.
|
||||||
func (d *Daemon) resolveKernelInputs(ctx context.Context, kernelRef, kernelPath, initrdPath, modulesDir string) (string, string, string, error) {
|
func (s *ImageService) resolveKernelInputs(ctx context.Context, kernelRef, kernelPath, initrdPath, modulesDir string) (string, string, string, error) {
|
||||||
kernelRef = strings.TrimSpace(kernelRef)
|
kernelRef = strings.TrimSpace(kernelRef)
|
||||||
kernelPath = strings.TrimSpace(kernelPath)
|
kernelPath = strings.TrimSpace(kernelPath)
|
||||||
initrdPath = strings.TrimSpace(initrdPath)
|
initrdPath = strings.TrimSpace(initrdPath)
|
||||||
|
|
@ -263,7 +263,7 @@ func (d *Daemon) resolveKernelInputs(ctx context.Context, kernelRef, kernelPath,
|
||||||
if kernelPath != "" || initrdPath != "" || modulesDir != "" {
|
if kernelPath != "" || initrdPath != "" || modulesDir != "" {
|
||||||
return "", "", "", fmt.Errorf("--kernel-ref is mutually exclusive with --kernel/--initrd/--modules")
|
return "", "", "", fmt.Errorf("--kernel-ref is mutually exclusive with --kernel/--initrd/--modules")
|
||||||
}
|
}
|
||||||
entry, err := d.readOrAutoPullKernel(ctx, kernelRef)
|
entry, err := s.readOrAutoPullKernel(ctx, kernelRef)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", err
|
return "", "", "", err
|
||||||
}
|
}
|
||||||
|
|
@ -278,8 +278,8 @@ func (d *Daemon) resolveKernelInputs(ctx context.Context, kernelRef, kernelPath,
|
||||||
|
|
||||||
// readOrAutoPullKernel tries the local kernelcat first; on miss, checks
|
// readOrAutoPullKernel tries the local kernelcat first; on miss, checks
|
||||||
// the embedded catalog and auto-pulls the bundle.
|
// the embedded catalog and auto-pulls the bundle.
|
||||||
func (d *Daemon) readOrAutoPullKernel(ctx context.Context, kernelRef string) (kernelcat.Entry, error) {
|
func (s *ImageService) readOrAutoPullKernel(ctx context.Context, kernelRef string) (kernelcat.Entry, error) {
|
||||||
entry, err := kernelcat.ReadLocal(d.layout.KernelsDir, kernelRef)
|
entry, err := kernelcat.ReadLocal(s.layout.KernelsDir, kernelRef)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return entry, nil
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
|
@ -294,8 +294,8 @@ func (d *Daemon) readOrAutoPullKernel(ctx context.Context, kernelRef string) (ke
|
||||||
return kernelcat.Entry{}, fmt.Errorf("kernel %q not found in catalog; run 'banger kernel list --available' to browse", kernelRef)
|
return kernelcat.Entry{}, fmt.Errorf("kernel %q not found in catalog; run 'banger kernel list --available' to browse", kernelRef)
|
||||||
}
|
}
|
||||||
vmCreateStage(ctx, "auto_pull_kernel", fmt.Sprintf("pulling kernel %s from catalog", kernelRef))
|
vmCreateStage(ctx, "auto_pull_kernel", fmt.Sprintf("pulling kernel %s from catalog", kernelRef))
|
||||||
if _, pullErr := d.KernelPull(ctx, api.KernelPullParams{Name: kernelRef}); pullErr != nil {
|
if _, pullErr := s.KernelPull(ctx, api.KernelPullParams{Name: kernelRef}); pullErr != nil {
|
||||||
return kernelcat.Entry{}, fmt.Errorf("auto-pull kernel %q: %w", kernelRef, pullErr)
|
return kernelcat.Entry{}, fmt.Errorf("auto-pull kernel %q: %w", kernelRef, pullErr)
|
||||||
}
|
}
|
||||||
return kernelcat.ReadLocal(d.layout.KernelsDir, kernelRef)
|
return kernelcat.ReadLocal(s.layout.KernelsDir, kernelRef)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ const minPullExt4Size int64 = 1 << 30 // 1 GiB
|
||||||
// staging dir to the final artifact dir, insert the store row. If two
|
// staging dir to the final artifact dir, insert the store row. If two
|
||||||
// pulls race to the same name, the loser fails fast at the recheck
|
// pulls race to the same name, the loser fails fast at the recheck
|
||||||
// and its staging dir is cleaned up via defer.
|
// and its staging dir is cleaned up via defer.
|
||||||
func (d *Daemon) PullImage(ctx context.Context, params api.ImagePullParams) (model.Image, error) {
|
func (s *ImageService) PullImage(ctx context.Context, params api.ImagePullParams) (model.Image, error) {
|
||||||
ref := strings.TrimSpace(params.Ref)
|
ref := strings.TrimSpace(params.Ref)
|
||||||
if ref == "" {
|
if ref == "" {
|
||||||
return model.Image{}, errors.New("reference is required")
|
return model.Image{}, errors.New("reference is required")
|
||||||
|
|
@ -55,9 +55,9 @@ func (d *Daemon) PullImage(ctx context.Context, params api.ImagePullParams) (mod
|
||||||
return model.Image{}, fmt.Errorf("load image catalog: %w", err)
|
return model.Image{}, fmt.Errorf("load image catalog: %w", err)
|
||||||
}
|
}
|
||||||
if entry, lookupErr := catalog.Lookup(ref); lookupErr == nil {
|
if entry, lookupErr := catalog.Lookup(ref); lookupErr == nil {
|
||||||
return d.pullFromBundle(ctx, params, entry)
|
return s.pullFromBundle(ctx, params, entry)
|
||||||
}
|
}
|
||||||
return d.pullFromOCI(ctx, params)
|
return s.pullFromOCI(ctx, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
// publishImage is the narrow critical section shared by every image-
|
// publishImage is the narrow critical section shared by every image-
|
||||||
|
|
@ -71,11 +71,11 @@ func (d *Daemon) PullImage(ctx context.Context, params api.ImagePullParams) (mod
|
||||||
// in place, e.g. RegisterImage which only touches the store). When
|
// in place, e.g. RegisterImage which only touches the store). When
|
||||||
// non-empty the rename is the publication atom: finalDir must not
|
// non-empty the rename is the publication atom: finalDir must not
|
||||||
// already exist before the rename fires.
|
// already exist before the rename fires.
|
||||||
func (d *Daemon) publishImage(ctx context.Context, image model.Image, stagingDir, finalDir string) (model.Image, error) {
|
func (s *ImageService) publishImage(ctx context.Context, image model.Image, stagingDir, finalDir string) (model.Image, error) {
|
||||||
d.imageOpsMu.Lock()
|
s.imageOpsMu.Lock()
|
||||||
defer d.imageOpsMu.Unlock()
|
defer s.imageOpsMu.Unlock()
|
||||||
|
|
||||||
if existing, err := d.store.GetImageByName(ctx, image.Name); err == nil {
|
if existing, err := s.store.GetImageByName(ctx, image.Name); err == nil {
|
||||||
return model.Image{}, fmt.Errorf("image %q already exists (id=%s); pick a different --name or delete it first", image.Name, existing.ID)
|
return model.Image{}, fmt.Errorf("image %q already exists (id=%s); pick a different --name or delete it first", image.Name, existing.ID)
|
||||||
}
|
}
|
||||||
if finalDir != "" {
|
if finalDir != "" {
|
||||||
|
|
@ -83,7 +83,7 @@ func (d *Daemon) publishImage(ctx context.Context, image model.Image, stagingDir
|
||||||
return model.Image{}, fmt.Errorf("publish artifact dir: %w", err)
|
return model.Image{}, fmt.Errorf("publish artifact dir: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := d.store.UpsertImage(ctx, image); err != nil {
|
if err := s.store.UpsertImage(ctx, image); err != nil {
|
||||||
if finalDir != "" {
|
if finalDir != "" {
|
||||||
_ = os.RemoveAll(finalDir)
|
_ = os.RemoveAll(finalDir)
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +94,7 @@ func (d *Daemon) publishImage(ctx context.Context, image model.Image, stagingDir
|
||||||
|
|
||||||
// pullFromOCI is the original OCI-registry-pull path. See PullImage for
|
// pullFromOCI is the original OCI-registry-pull path. See PullImage for
|
||||||
// the intent.
|
// the intent.
|
||||||
func (d *Daemon) pullFromOCI(ctx context.Context, params api.ImagePullParams) (image model.Image, err error) {
|
func (s *ImageService) pullFromOCI(ctx context.Context, params api.ImagePullParams) (image model.Image, err error) {
|
||||||
ref := strings.TrimSpace(params.Ref)
|
ref := strings.TrimSpace(params.Ref)
|
||||||
parsed, err := name.ParseReference(ref)
|
parsed, err := name.ParseReference(ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -108,11 +108,11 @@ func (d *Daemon) pullFromOCI(ctx context.Context, params api.ImagePullParams) (i
|
||||||
return model.Image{}, errors.New("could not derive image name from ref; pass --name")
|
return model.Image{}, errors.New("could not derive image name from ref; pass --name")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if existing, lookupErr := d.store.GetImageByName(ctx, imgName); lookupErr == nil {
|
if existing, lookupErr := s.store.GetImageByName(ctx, imgName); lookupErr == nil {
|
||||||
return model.Image{}, fmt.Errorf("image %q already exists (id=%s); pick a different --name or delete it first", imgName, existing.ID)
|
return model.Image{}, fmt.Errorf("image %q already exists (id=%s); pick a different --name or delete it first", imgName, existing.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
kernelPath, initrdPath, modulesDir, err := d.resolveKernelInputs(ctx, params.KernelRef, params.KernelPath, params.InitrdPath, params.ModulesDir)
|
kernelPath, initrdPath, modulesDir, err := s.resolveKernelInputs(ctx, params.KernelRef, params.KernelPath, params.InitrdPath, params.ModulesDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -124,7 +124,7 @@ func (d *Daemon) pullFromOCI(ctx context.Context, params api.ImagePullParams) (i
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
finalDir := filepath.Join(d.layout.ImagesDir, id)
|
finalDir := filepath.Join(s.layout.ImagesDir, id)
|
||||||
stagingDir := finalDir + ".staging"
|
stagingDir := finalDir + ".staging"
|
||||||
if err := os.MkdirAll(stagingDir, 0o755); err != nil {
|
if err := os.MkdirAll(stagingDir, 0o755); err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
|
|
@ -144,7 +144,7 @@ func (d *Daemon) pullFromOCI(ctx context.Context, params api.ImagePullParams) (i
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(rootfsTree)
|
defer os.RemoveAll(rootfsTree)
|
||||||
|
|
||||||
meta, err := d.runPullAndFlatten(ctx, ref, d.layout.OCICacheDir, rootfsTree)
|
meta, err := s.runPullAndFlatten(ctx, ref, s.layout.OCICacheDir, rootfsTree)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Image{}, fmt.Errorf("pull oci image: %w", err)
|
return model.Image{}, fmt.Errorf("pull oci image: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -162,14 +162,14 @@ func (d *Daemon) pullFromOCI(ctx context.Context, params api.ImagePullParams) (i
|
||||||
}
|
}
|
||||||
|
|
||||||
rootfsExt4 := filepath.Join(stagingDir, "rootfs.ext4")
|
rootfsExt4 := filepath.Join(stagingDir, "rootfs.ext4")
|
||||||
if err := imagepull.BuildExt4(ctx, d.runner, rootfsTree, rootfsExt4, sizeBytes); err != nil {
|
if err := imagepull.BuildExt4(ctx, s.runner, rootfsTree, rootfsExt4, sizeBytes); err != nil {
|
||||||
return model.Image{}, fmt.Errorf("build rootfs ext4: %w", err)
|
return model.Image{}, fmt.Errorf("build rootfs ext4: %w", err)
|
||||||
}
|
}
|
||||||
if err := d.runFinalizePulledRootfs(ctx, rootfsExt4, meta); err != nil {
|
if err := s.runFinalizePulledRootfs(ctx, rootfsExt4, meta); err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
stagedKernel, stagedInitrd, stagedModules, err := imagemgr.StageBootArtifacts(ctx, d.runner, stagingDir, kernelPath, initrdPath, modulesDir)
|
stagedKernel, stagedInitrd, stagedModules, err := imagemgr.StageBootArtifacts(ctx, s.runner, stagingDir, kernelPath, initrdPath, modulesDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Image{}, fmt.Errorf("stage boot artifacts: %w", err)
|
return model.Image{}, fmt.Errorf("stage boot artifacts: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -187,7 +187,7 @@ func (d *Daemon) pullFromOCI(ctx context.Context, params api.ImagePullParams) (i
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
published, err := d.publishImage(ctx, image, stagingDir, finalDir)
|
published, err := s.publishImage(ctx, image, stagingDir, finalDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -200,12 +200,12 @@ func (d *Daemon) pullFromOCI(ctx context.Context, params api.ImagePullParams) (i
|
||||||
// injected at build time), verify its sha256, and register the result
|
// injected at build time), verify its sha256, and register the result
|
||||||
// as a managed image. No flatten / mkfs / debugfs work on the daemon
|
// as a managed image. No flatten / mkfs / debugfs work on the daemon
|
||||||
// host.
|
// host.
|
||||||
func (d *Daemon) pullFromBundle(ctx context.Context, params api.ImagePullParams, entry imagecat.CatEntry) (image model.Image, err error) {
|
func (s *ImageService) pullFromBundle(ctx context.Context, params api.ImagePullParams, entry imagecat.CatEntry) (image model.Image, err error) {
|
||||||
imgName := strings.TrimSpace(params.Name)
|
imgName := strings.TrimSpace(params.Name)
|
||||||
if imgName == "" {
|
if imgName == "" {
|
||||||
imgName = entry.Name
|
imgName = entry.Name
|
||||||
}
|
}
|
||||||
if existing, lookupErr := d.store.GetImageByName(ctx, imgName); lookupErr == nil {
|
if existing, lookupErr := s.store.GetImageByName(ctx, imgName); lookupErr == nil {
|
||||||
return model.Image{}, fmt.Errorf("image %q already exists (id=%s); pick a different --name or delete it first", imgName, existing.ID)
|
return model.Image{}, fmt.Errorf("image %q already exists (id=%s); pick a different --name or delete it first", imgName, existing.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,7 +214,7 @@ func (d *Daemon) pullFromBundle(ctx context.Context, params api.ImagePullParams,
|
||||||
if kernelRef == "" && strings.TrimSpace(params.KernelPath) == "" {
|
if kernelRef == "" && strings.TrimSpace(params.KernelPath) == "" {
|
||||||
kernelRef = strings.TrimSpace(entry.KernelRef)
|
kernelRef = strings.TrimSpace(entry.KernelRef)
|
||||||
}
|
}
|
||||||
kernelPath, initrdPath, modulesDir, err := d.resolveKernelInputs(ctx, kernelRef, params.KernelPath, params.InitrdPath, params.ModulesDir)
|
kernelPath, initrdPath, modulesDir, err := s.resolveKernelInputs(ctx, kernelRef, params.KernelPath, params.InitrdPath, params.ModulesDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -226,7 +226,7 @@ func (d *Daemon) pullFromBundle(ctx context.Context, params api.ImagePullParams,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
finalDir := filepath.Join(d.layout.ImagesDir, id)
|
finalDir := filepath.Join(s.layout.ImagesDir, id)
|
||||||
stagingDir := finalDir + ".staging"
|
stagingDir := finalDir + ".staging"
|
||||||
if err := os.MkdirAll(stagingDir, 0o755); err != nil {
|
if err := os.MkdirAll(stagingDir, 0o755); err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
|
|
@ -238,7 +238,7 @@ func (d *Daemon) pullFromBundle(ctx context.Context, params api.ImagePullParams,
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if _, err := d.runBundleFetch(ctx, stagingDir, entry); err != nil {
|
if _, err := s.runBundleFetch(ctx, stagingDir, entry); err != nil {
|
||||||
return model.Image{}, fmt.Errorf("fetch bundle: %w", err)
|
return model.Image{}, fmt.Errorf("fetch bundle: %w", err)
|
||||||
}
|
}
|
||||||
// manifest.json is metadata we only need at fetch time; strip it
|
// manifest.json is metadata we only need at fetch time; strip it
|
||||||
|
|
@ -246,7 +246,7 @@ func (d *Daemon) pullFromBundle(ctx context.Context, params api.ImagePullParams,
|
||||||
_ = os.Remove(filepath.Join(stagingDir, imagecat.ManifestFilename))
|
_ = os.Remove(filepath.Join(stagingDir, imagecat.ManifestFilename))
|
||||||
rootfsExt4 := filepath.Join(stagingDir, imagecat.RootfsFilename)
|
rootfsExt4 := filepath.Join(stagingDir, imagecat.RootfsFilename)
|
||||||
|
|
||||||
stagedKernel, stagedInitrd, stagedModules, err := imagemgr.StageBootArtifacts(ctx, d.runner, stagingDir, kernelPath, initrdPath, modulesDir)
|
stagedKernel, stagedInitrd, stagedModules, err := imagemgr.StageBootArtifacts(ctx, s.runner, stagingDir, kernelPath, initrdPath, modulesDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Image{}, fmt.Errorf("stage boot artifacts: %w", err)
|
return model.Image{}, fmt.Errorf("stage boot artifacts: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -264,7 +264,7 @@ func (d *Daemon) pullFromBundle(ctx context.Context, params api.ImagePullParams,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
published, err := d.publishImage(ctx, image, stagingDir, finalDir)
|
published, err := s.publishImage(ctx, image, stagingDir, finalDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -273,17 +273,17 @@ func (d *Daemon) pullFromBundle(ctx context.Context, params api.ImagePullParams,
|
||||||
}
|
}
|
||||||
|
|
||||||
// runBundleFetch is the seam tests substitute. nil → real implementation.
|
// runBundleFetch is the seam tests substitute. nil → real implementation.
|
||||||
func (d *Daemon) runBundleFetch(ctx context.Context, destDir string, entry imagecat.CatEntry) (imagecat.Manifest, error) {
|
func (s *ImageService) runBundleFetch(ctx context.Context, destDir string, entry imagecat.CatEntry) (imagecat.Manifest, error) {
|
||||||
if d.bundleFetch != nil {
|
if s.bundleFetch != nil {
|
||||||
return d.bundleFetch(ctx, destDir, entry)
|
return s.bundleFetch(ctx, destDir, entry)
|
||||||
}
|
}
|
||||||
return imagecat.Fetch(ctx, nil, destDir, entry)
|
return imagecat.Fetch(ctx, nil, destDir, entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
// runPullAndFlatten is the seam tests substitute. nil → real implementation.
|
// runPullAndFlatten is the seam tests substitute. nil → real implementation.
|
||||||
func (d *Daemon) runPullAndFlatten(ctx context.Context, ref, cacheDir, destDir string) (imagepull.Metadata, error) {
|
func (s *ImageService) runPullAndFlatten(ctx context.Context, ref, cacheDir, destDir string) (imagepull.Metadata, error) {
|
||||||
if d.pullAndFlatten != nil {
|
if s.pullAndFlatten != nil {
|
||||||
return d.pullAndFlatten(ctx, ref, cacheDir, destDir)
|
return s.pullAndFlatten(ctx, ref, cacheDir, destDir)
|
||||||
}
|
}
|
||||||
pulled, err := imagepull.Pull(ctx, ref, cacheDir)
|
pulled, err := imagepull.Pull(ctx, ref, cacheDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -293,21 +293,21 @@ func (d *Daemon) runPullAndFlatten(ctx context.Context, ref, cacheDir, destDir s
|
||||||
}
|
}
|
||||||
|
|
||||||
// runFinalizePulledRootfs applies ownership fixup and injects banger's
|
// runFinalizePulledRootfs applies ownership fixup and injects banger's
|
||||||
// guest agents. Tests substitute via d.finalizePulledRootfs; nil →
|
// guest agents. Tests substitute via s.finalizePulledRootfs; nil →
|
||||||
// real implementation using debugfs + the companion vsock-agent
|
// real implementation using debugfs + the companion vsock-agent
|
||||||
// binary resolved via paths.CompanionBinaryPath.
|
// binary resolved via paths.CompanionBinaryPath.
|
||||||
func (d *Daemon) runFinalizePulledRootfs(ctx context.Context, ext4File string, meta imagepull.Metadata) error {
|
func (s *ImageService) runFinalizePulledRootfs(ctx context.Context, ext4File string, meta imagepull.Metadata) error {
|
||||||
if d.finalizePulledRootfs != nil {
|
if s.finalizePulledRootfs != nil {
|
||||||
return d.finalizePulledRootfs(ctx, ext4File, meta)
|
return s.finalizePulledRootfs(ctx, ext4File, meta)
|
||||||
}
|
}
|
||||||
if err := imagepull.ApplyOwnership(ctx, d.runner, ext4File, meta); err != nil {
|
if err := imagepull.ApplyOwnership(ctx, s.runner, ext4File, meta); err != nil {
|
||||||
return fmt.Errorf("apply ownership: %w", err)
|
return fmt.Errorf("apply ownership: %w", err)
|
||||||
}
|
}
|
||||||
vsockBin, err := paths.CompanionBinaryPath("banger-vsock-agent")
|
vsockBin, err := paths.CompanionBinaryPath("banger-vsock-agent")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("locate vsock agent binary: %w", err)
|
return fmt.Errorf("locate vsock agent binary: %w", err)
|
||||||
}
|
}
|
||||||
if err := imagepull.InjectGuestAgents(ctx, d.runner, ext4File, imagepull.GuestAgentAssets{
|
if err := imagepull.InjectGuestAgents(ctx, s.runner, ext4File, imagepull.GuestAgentAssets{
|
||||||
VsockAgentBin: vsockBin,
|
VsockAgentBin: vsockBin,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("inject guest agents: %w", err)
|
return fmt.Errorf("inject guest agents: %w", err)
|
||||||
|
|
|
||||||
|
|
@ -63,9 +63,14 @@ func TestPullImageBundlePathRegistersFromCatalog(t *testing.T) {
|
||||||
seedKernel(t, kernelsDir, "generic-6.12")
|
seedKernel(t, kernelsDir, "generic-6.12")
|
||||||
|
|
||||||
d := &Daemon{
|
d := &Daemon{
|
||||||
layout: paths.Layout{ImagesDir: imagesDir, KernelsDir: kernelsDir},
|
layout: paths.Layout{ImagesDir: imagesDir, KernelsDir: kernelsDir},
|
||||||
store: openDaemonStore(t),
|
store: openDaemonStore(t),
|
||||||
runner: system.NewRunner(),
|
runner: system.NewRunner(),
|
||||||
|
}
|
||||||
|
d.img = &ImageService{
|
||||||
|
layout: d.layout,
|
||||||
|
store: d.store,
|
||||||
|
runner: d.runner,
|
||||||
bundleFetch: stubBundleFetch(imagecat.Manifest{KernelRef: "generic-6.12"}),
|
bundleFetch: stubBundleFetch(imagecat.Manifest{KernelRef: "generic-6.12"}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,7 +82,7 @@ func TestPullImageBundlePathRegistersFromCatalog(t *testing.T) {
|
||||||
TarballURL: "https://example.com/x.tar.zst",
|
TarballURL: "https://example.com/x.tar.zst",
|
||||||
TarballSHA256: "abc",
|
TarballSHA256: "abc",
|
||||||
}
|
}
|
||||||
image, err := d.pullFromBundle(context.Background(), api.ImagePullParams{Ref: "debian-bookworm"}, entry)
|
image, err := d.img.pullFromBundle(context.Background(), api.ImagePullParams{Ref: "debian-bookworm"}, entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("pullFromBundle: %v", err)
|
t.Fatalf("pullFromBundle: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -111,9 +116,14 @@ func TestPullImageBundlePathOverrideNameAndKernelRef(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
d := &Daemon{
|
d := &Daemon{
|
||||||
layout: paths.Layout{ImagesDir: imagesDir, KernelsDir: kernelsDir},
|
layout: paths.Layout{ImagesDir: imagesDir, KernelsDir: kernelsDir},
|
||||||
store: openDaemonStore(t),
|
store: openDaemonStore(t),
|
||||||
runner: system.NewRunner(),
|
runner: system.NewRunner(),
|
||||||
|
}
|
||||||
|
d.img = &ImageService{
|
||||||
|
layout: d.layout,
|
||||||
|
store: d.store,
|
||||||
|
runner: d.runner,
|
||||||
bundleFetch: stubBundleFetch(imagecat.Manifest{KernelRef: "generic-6.12"}),
|
bundleFetch: stubBundleFetch(imagecat.Manifest{KernelRef: "generic-6.12"}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,7 +133,7 @@ func TestPullImageBundlePathOverrideNameAndKernelRef(t *testing.T) {
|
||||||
TarballURL: "https://example.com/x.tar.zst",
|
TarballURL: "https://example.com/x.tar.zst",
|
||||||
TarballSHA256: "abc",
|
TarballSHA256: "abc",
|
||||||
}
|
}
|
||||||
image, err := d.pullFromBundle(context.Background(), api.ImagePullParams{
|
image, err := d.img.pullFromBundle(context.Background(), api.ImagePullParams{
|
||||||
Ref: "debian-bookworm", Name: "my-sandbox", KernelRef: "custom-kernel",
|
Ref: "debian-bookworm", Name: "my-sandbox", KernelRef: "custom-kernel",
|
||||||
}, entry)
|
}, entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -147,9 +157,14 @@ func TestPullImageBundlePathRejectsExistingName(t *testing.T) {
|
||||||
seedKernel(t, kernelsDir, "generic-6.12")
|
seedKernel(t, kernelsDir, "generic-6.12")
|
||||||
|
|
||||||
d := &Daemon{
|
d := &Daemon{
|
||||||
layout: paths.Layout{ImagesDir: imagesDir, KernelsDir: kernelsDir},
|
layout: paths.Layout{ImagesDir: imagesDir, KernelsDir: kernelsDir},
|
||||||
store: openDaemonStore(t),
|
store: openDaemonStore(t),
|
||||||
runner: system.NewRunner(),
|
runner: system.NewRunner(),
|
||||||
|
}
|
||||||
|
d.img = &ImageService{
|
||||||
|
layout: d.layout,
|
||||||
|
store: d.store,
|
||||||
|
runner: d.runner,
|
||||||
bundleFetch: stubBundleFetch(imagecat.Manifest{KernelRef: "generic-6.12"}),
|
bundleFetch: stubBundleFetch(imagecat.Manifest{KernelRef: "generic-6.12"}),
|
||||||
}
|
}
|
||||||
id, _ := model.NewID()
|
id, _ := model.NewID()
|
||||||
|
|
@ -160,7 +175,7 @@ func TestPullImageBundlePathRejectsExistingName(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := d.pullFromBundle(context.Background(), api.ImagePullParams{Ref: "debian-bookworm"}, imagecat.CatEntry{
|
_, err := d.img.pullFromBundle(context.Background(), api.ImagePullParams{Ref: "debian-bookworm"}, imagecat.CatEntry{
|
||||||
Name: "debian-bookworm", KernelRef: "generic-6.12",
|
Name: "debian-bookworm", KernelRef: "generic-6.12",
|
||||||
TarballURL: "https://example.com/x.tar.zst", TarballSHA256: "abc",
|
TarballURL: "https://example.com/x.tar.zst", TarballSHA256: "abc",
|
||||||
})
|
})
|
||||||
|
|
@ -171,13 +186,18 @@ func TestPullImageBundlePathRejectsExistingName(t *testing.T) {
|
||||||
|
|
||||||
func TestPullImageBundlePathRequiresSomeKernelSource(t *testing.T) {
|
func TestPullImageBundlePathRequiresSomeKernelSource(t *testing.T) {
|
||||||
d := &Daemon{
|
d := &Daemon{
|
||||||
layout: paths.Layout{ImagesDir: t.TempDir(), KernelsDir: t.TempDir()},
|
layout: paths.Layout{ImagesDir: t.TempDir(), KernelsDir: t.TempDir()},
|
||||||
store: openDaemonStore(t),
|
store: openDaemonStore(t),
|
||||||
runner: system.NewRunner(),
|
runner: system.NewRunner(),
|
||||||
|
}
|
||||||
|
d.img = &ImageService{
|
||||||
|
layout: d.layout,
|
||||||
|
store: d.store,
|
||||||
|
runner: d.runner,
|
||||||
bundleFetch: stubBundleFetch(imagecat.Manifest{}),
|
bundleFetch: stubBundleFetch(imagecat.Manifest{}),
|
||||||
}
|
}
|
||||||
// Catalog entry has no kernel_ref, no --kernel-ref/--kernel passed.
|
// Catalog entry has no kernel_ref, no --kernel-ref/--kernel passed.
|
||||||
_, err := d.pullFromBundle(context.Background(), api.ImagePullParams{Ref: "x"}, imagecat.CatEntry{
|
_, err := d.img.pullFromBundle(context.Background(), api.ImagePullParams{Ref: "x"}, imagecat.CatEntry{
|
||||||
Name: "x", TarballURL: "https://example.com/x.tar.zst", TarballSHA256: "abc",
|
Name: "x", TarballURL: "https://example.com/x.tar.zst", TarballSHA256: "abc",
|
||||||
})
|
})
|
||||||
if err == nil || !strings.Contains(err.Error(), "kernel") {
|
if err == nil || !strings.Contains(err.Error(), "kernel") {
|
||||||
|
|
@ -194,11 +214,16 @@ func TestPullImageBundleFetchFailurePropagates(t *testing.T) {
|
||||||
layout: paths.Layout{ImagesDir: imagesDir, KernelsDir: kernelsDir},
|
layout: paths.Layout{ImagesDir: imagesDir, KernelsDir: kernelsDir},
|
||||||
store: openDaemonStore(t),
|
store: openDaemonStore(t),
|
||||||
runner: system.NewRunner(),
|
runner: system.NewRunner(),
|
||||||
|
}
|
||||||
|
d.img = &ImageService{
|
||||||
|
layout: d.layout,
|
||||||
|
store: d.store,
|
||||||
|
runner: d.runner,
|
||||||
bundleFetch: func(_ context.Context, _ string, _ imagecat.CatEntry) (imagecat.Manifest, error) {
|
bundleFetch: func(_ context.Context, _ string, _ imagecat.CatEntry) (imagecat.Manifest, error) {
|
||||||
return imagecat.Manifest{}, errors.New("r2 exploded")
|
return imagecat.Manifest{}, errors.New("r2 exploded")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
_, err := d.pullFromBundle(context.Background(), api.ImagePullParams{Ref: "x"}, imagecat.CatEntry{
|
_, err := d.img.pullFromBundle(context.Background(), api.ImagePullParams{Ref: "x"}, imagecat.CatEntry{
|
||||||
Name: "x", KernelRef: "generic-6.12",
|
Name: "x", KernelRef: "generic-6.12",
|
||||||
TarballURL: "https://example.com/x.tar.zst", TarballSHA256: "abc",
|
TarballURL: "https://example.com/x.tar.zst", TarballSHA256: "abc",
|
||||||
})
|
})
|
||||||
|
|
@ -222,6 +247,11 @@ func TestPullImageDispatchFallsThroughToOCIWhenNoCatalogHit(t *testing.T) {
|
||||||
layout: paths.Layout{ImagesDir: imagesDir, KernelsDir: kernelsDir, OCICacheDir: t.TempDir()},
|
layout: paths.Layout{ImagesDir: imagesDir, KernelsDir: kernelsDir, OCICacheDir: t.TempDir()},
|
||||||
store: openDaemonStore(t),
|
store: openDaemonStore(t),
|
||||||
runner: system.NewRunner(),
|
runner: system.NewRunner(),
|
||||||
|
}
|
||||||
|
d.img = &ImageService{
|
||||||
|
layout: d.layout,
|
||||||
|
store: d.store,
|
||||||
|
runner: d.runner,
|
||||||
pullAndFlatten: func(_ context.Context, ref, _ string, destDir string) (imagepull.Metadata, error) {
|
pullAndFlatten: func(_ context.Context, ref, _ string, destDir string) (imagepull.Metadata, error) {
|
||||||
ociCalled = true
|
ociCalled = true
|
||||||
if err := os.WriteFile(filepath.Join(destDir, "marker"), []byte("x"), 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(destDir, "marker"), []byte("x"), 0o644); err != nil {
|
||||||
|
|
@ -233,7 +263,7 @@ func TestPullImageDispatchFallsThroughToOCIWhenNoCatalogHit(t *testing.T) {
|
||||||
bundleFetch: stubBundleFetch(imagecat.Manifest{}),
|
bundleFetch: stubBundleFetch(imagecat.Manifest{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := d.PullImage(context.Background(), api.ImagePullParams{
|
_, err := d.img.PullImage(context.Background(), api.ImagePullParams{
|
||||||
// Not a catalog name (catalog is empty in the embedded default).
|
// Not a catalog name (catalog is empty in the embedded default).
|
||||||
Ref: "docker.io/library/debian:bookworm",
|
Ref: "docker.io/library/debian:bookworm",
|
||||||
KernelRef: "generic-6.12",
|
KernelRef: "generic-6.12",
|
||||||
|
|
|
||||||
|
|
@ -71,14 +71,19 @@ func TestPullImageHappyPath(t *testing.T) {
|
||||||
kernel, initrd, modules := writeFakeKernelTriple(t)
|
kernel, initrd, modules := writeFakeKernelTriple(t)
|
||||||
|
|
||||||
d := &Daemon{
|
d := &Daemon{
|
||||||
layout: paths.Layout{ImagesDir: imagesDir, OCICacheDir: cacheDir},
|
layout: paths.Layout{ImagesDir: imagesDir, OCICacheDir: cacheDir},
|
||||||
store: openDaemonStore(t),
|
store: openDaemonStore(t),
|
||||||
runner: system.NewRunner(),
|
runner: system.NewRunner(),
|
||||||
|
}
|
||||||
|
d.img = &ImageService{
|
||||||
|
layout: d.layout,
|
||||||
|
store: d.store,
|
||||||
|
runner: d.runner,
|
||||||
pullAndFlatten: stubPullAndFlatten,
|
pullAndFlatten: stubPullAndFlatten,
|
||||||
finalizePulledRootfs: stubFinalizePulledRootfs,
|
finalizePulledRootfs: stubFinalizePulledRootfs,
|
||||||
}
|
}
|
||||||
|
|
||||||
image, err := d.PullImage(context.Background(), api.ImagePullParams{
|
image, err := d.img.PullImage(context.Background(), api.ImagePullParams{
|
||||||
Ref: "docker.io/library/debian:bookworm",
|
Ref: "docker.io/library/debian:bookworm",
|
||||||
KernelPath: kernel,
|
KernelPath: kernel,
|
||||||
InitrdPath: initrd,
|
InitrdPath: initrd,
|
||||||
|
|
@ -116,9 +121,14 @@ func TestPullImageRejectsExistingName(t *testing.T) {
|
||||||
kernel, _, _ := writeFakeKernelTriple(t)
|
kernel, _, _ := writeFakeKernelTriple(t)
|
||||||
|
|
||||||
d := &Daemon{
|
d := &Daemon{
|
||||||
layout: paths.Layout{ImagesDir: imagesDir, OCICacheDir: t.TempDir()},
|
layout: paths.Layout{ImagesDir: imagesDir, OCICacheDir: t.TempDir()},
|
||||||
store: openDaemonStore(t),
|
store: openDaemonStore(t),
|
||||||
runner: system.NewRunner(),
|
runner: system.NewRunner(),
|
||||||
|
}
|
||||||
|
d.img = &ImageService{
|
||||||
|
layout: d.layout,
|
||||||
|
store: d.store,
|
||||||
|
runner: d.runner,
|
||||||
pullAndFlatten: stubPullAndFlatten,
|
pullAndFlatten: stubPullAndFlatten,
|
||||||
finalizePulledRootfs: stubFinalizePulledRootfs,
|
finalizePulledRootfs: stubFinalizePulledRootfs,
|
||||||
}
|
}
|
||||||
|
|
@ -133,7 +143,7 @@ func TestPullImageRejectsExistingName(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := d.PullImage(context.Background(), api.ImagePullParams{
|
_, err := d.img.PullImage(context.Background(), api.ImagePullParams{
|
||||||
Ref: "docker.io/library/debian:bookworm",
|
Ref: "docker.io/library/debian:bookworm",
|
||||||
KernelPath: kernel,
|
KernelPath: kernel,
|
||||||
})
|
})
|
||||||
|
|
@ -144,13 +154,18 @@ func TestPullImageRejectsExistingName(t *testing.T) {
|
||||||
|
|
||||||
func TestPullImageRequiresKernel(t *testing.T) {
|
func TestPullImageRequiresKernel(t *testing.T) {
|
||||||
d := &Daemon{
|
d := &Daemon{
|
||||||
layout: paths.Layout{ImagesDir: t.TempDir(), OCICacheDir: t.TempDir()},
|
layout: paths.Layout{ImagesDir: t.TempDir(), OCICacheDir: t.TempDir()},
|
||||||
store: openDaemonStore(t),
|
store: openDaemonStore(t),
|
||||||
runner: system.NewRunner(),
|
runner: system.NewRunner(),
|
||||||
|
}
|
||||||
|
d.img = &ImageService{
|
||||||
|
layout: d.layout,
|
||||||
|
store: d.store,
|
||||||
|
runner: d.runner,
|
||||||
pullAndFlatten: stubPullAndFlatten,
|
pullAndFlatten: stubPullAndFlatten,
|
||||||
finalizePulledRootfs: stubFinalizePulledRootfs,
|
finalizePulledRootfs: stubFinalizePulledRootfs,
|
||||||
}
|
}
|
||||||
_, err := d.PullImage(context.Background(), api.ImagePullParams{
|
_, err := d.img.PullImage(context.Background(), api.ImagePullParams{
|
||||||
Ref: "docker.io/library/debian:bookworm",
|
Ref: "docker.io/library/debian:bookworm",
|
||||||
})
|
})
|
||||||
if err == nil || !strings.Contains(err.Error(), "kernel") {
|
if err == nil || !strings.Contains(err.Error(), "kernel") {
|
||||||
|
|
@ -166,13 +181,18 @@ func TestPullImageCleansStagingOnFailure(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
d := &Daemon{
|
d := &Daemon{
|
||||||
layout: paths.Layout{ImagesDir: imagesDir, OCICacheDir: t.TempDir()},
|
layout: paths.Layout{ImagesDir: imagesDir, OCICacheDir: t.TempDir()},
|
||||||
store: openDaemonStore(t),
|
store: openDaemonStore(t),
|
||||||
runner: system.NewRunner(),
|
runner: system.NewRunner(),
|
||||||
|
}
|
||||||
|
d.img = &ImageService{
|
||||||
|
layout: d.layout,
|
||||||
|
store: d.store,
|
||||||
|
runner: d.runner,
|
||||||
pullAndFlatten: failureSeam,
|
pullAndFlatten: failureSeam,
|
||||||
finalizePulledRootfs: stubFinalizePulledRootfs,
|
finalizePulledRootfs: stubFinalizePulledRootfs,
|
||||||
}
|
}
|
||||||
_, err := d.PullImage(context.Background(), api.ImagePullParams{
|
_, err := d.img.PullImage(context.Background(), api.ImagePullParams{
|
||||||
Ref: "docker.io/library/debian:bookworm",
|
Ref: "docker.io/library/debian:bookworm",
|
||||||
KernelPath: kernel,
|
KernelPath: kernel,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ import (
|
||||||
"banger/internal/system"
|
"banger/internal/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *Daemon) KernelList(_ context.Context) (api.KernelListResult, error) {
|
func (s *ImageService) KernelList(_ context.Context) (api.KernelListResult, error) {
|
||||||
entries, err := kernelcat.ListLocal(d.layout.KernelsDir)
|
entries, err := kernelcat.ListLocal(s.layout.KernelsDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return api.KernelListResult{}, err
|
return api.KernelListResult{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -26,19 +26,19 @@ func (d *Daemon) KernelList(_ context.Context) (api.KernelListResult, error) {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Daemon) KernelShow(_ context.Context, name string) (api.KernelEntry, error) {
|
func (s *ImageService) KernelShow(_ context.Context, name string) (api.KernelEntry, error) {
|
||||||
entry, err := kernelcat.ReadLocal(d.layout.KernelsDir, name)
|
entry, err := kernelcat.ReadLocal(s.layout.KernelsDir, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return api.KernelEntry{}, kernelNotFoundIfMissing(name, err)
|
return api.KernelEntry{}, kernelNotFoundIfMissing(name, err)
|
||||||
}
|
}
|
||||||
return kernelEntryToAPI(entry), nil
|
return kernelEntryToAPI(entry), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Daemon) KernelDelete(_ context.Context, name string) error {
|
func (s *ImageService) KernelDelete(_ context.Context, name string) error {
|
||||||
if err := kernelcat.ValidateName(name); err != nil {
|
if err := kernelcat.ValidateName(name); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return kernelcat.DeleteLocal(d.layout.KernelsDir, name)
|
return kernelcat.DeleteLocal(s.layout.KernelsDir, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// KernelImport copies the kernel / initrd / modules artifacts produced by
|
// KernelImport copies the kernel / initrd / modules artifacts produced by
|
||||||
|
|
@ -46,7 +46,7 @@ func (d *Daemon) KernelDelete(_ context.Context, name string) error {
|
||||||
// under params.Name and writes the manifest. It is the primary bridge from
|
// under params.Name and writes the manifest. It is the primary bridge from
|
||||||
// "I built a kernel with the helper scripts" to "banger kernel list shows
|
// "I built a kernel with the helper scripts" to "banger kernel list shows
|
||||||
// it and image register --kernel-ref works."
|
// it and image register --kernel-ref works."
|
||||||
func (d *Daemon) KernelImport(ctx context.Context, params api.KernelImportParams) (api.KernelEntry, error) {
|
func (s *ImageService) KernelImport(ctx context.Context, params api.KernelImportParams) (api.KernelEntry, error) {
|
||||||
name := strings.TrimSpace(params.Name)
|
name := strings.TrimSpace(params.Name)
|
||||||
if err := kernelcat.ValidateName(name); err != nil {
|
if err := kernelcat.ValidateName(name); err != nil {
|
||||||
return api.KernelEntry{}, err
|
return api.KernelEntry{}, err
|
||||||
|
|
@ -61,9 +61,9 @@ func (d *Daemon) KernelImport(ctx context.Context, params api.KernelImportParams
|
||||||
return api.KernelEntry{}, fmt.Errorf("discover artifacts under %s: %w", fromDir, err)
|
return api.KernelEntry{}, fmt.Errorf("discover artifacts under %s: %w", fromDir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
targetDir := kernelcat.EntryDir(d.layout.KernelsDir, name)
|
targetDir := kernelcat.EntryDir(s.layout.KernelsDir, name)
|
||||||
// Overwrite-by-default: clear any prior entry so a re-import is clean.
|
// Overwrite-by-default: clear any prior entry so a re-import is clean.
|
||||||
if err := kernelcat.DeleteLocal(d.layout.KernelsDir, name); err != nil {
|
if err := kernelcat.DeleteLocal(s.layout.KernelsDir, name); err != nil {
|
||||||
return api.KernelEntry{}, fmt.Errorf("clear prior catalog entry %q: %w", name, err)
|
return api.KernelEntry{}, fmt.Errorf("clear prior catalog entry %q: %w", name, err)
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
||||||
|
|
@ -85,7 +85,7 @@ func (d *Daemon) KernelImport(ctx context.Context, params api.KernelImportParams
|
||||||
if err := os.MkdirAll(modulesTarget, 0o755); err != nil {
|
if err := os.MkdirAll(modulesTarget, 0o755); err != nil {
|
||||||
return api.KernelEntry{}, err
|
return api.KernelEntry{}, err
|
||||||
}
|
}
|
||||||
if err := system.CopyDirContents(ctx, d.runner, discovered.ModulesDir, modulesTarget, false); err != nil {
|
if err := system.CopyDirContents(ctx, s.runner, discovered.ModulesDir, modulesTarget, false); err != nil {
|
||||||
return api.KernelEntry{}, fmt.Errorf("copy modules: %w", err)
|
return api.KernelEntry{}, fmt.Errorf("copy modules: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -104,10 +104,10 @@ func (d *Daemon) KernelImport(ctx context.Context, params api.KernelImportParams
|
||||||
Source: "import:" + fromDir,
|
Source: "import:" + fromDir,
|
||||||
ImportedAt: time.Now().UTC(),
|
ImportedAt: time.Now().UTC(),
|
||||||
}
|
}
|
||||||
if err := kernelcat.WriteLocal(d.layout.KernelsDir, entry); err != nil {
|
if err := kernelcat.WriteLocal(s.layout.KernelsDir, entry); err != nil {
|
||||||
return api.KernelEntry{}, fmt.Errorf("write manifest: %w", err)
|
return api.KernelEntry{}, fmt.Errorf("write manifest: %w", err)
|
||||||
}
|
}
|
||||||
stored, err := kernelcat.ReadLocal(d.layout.KernelsDir, name)
|
stored, err := kernelcat.ReadLocal(s.layout.KernelsDir, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return api.KernelEntry{}, err
|
return api.KernelEntry{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -116,14 +116,14 @@ func (d *Daemon) KernelImport(ctx context.Context, params api.KernelImportParams
|
||||||
|
|
||||||
// KernelPull downloads a catalog entry by name into the local catalog. It
|
// KernelPull downloads a catalog entry by name into the local catalog. It
|
||||||
// refuses to overwrite an existing entry unless params.Force is set.
|
// refuses to overwrite an existing entry unless params.Force is set.
|
||||||
func (d *Daemon) KernelPull(ctx context.Context, params api.KernelPullParams) (api.KernelEntry, error) {
|
func (s *ImageService) KernelPull(ctx context.Context, params api.KernelPullParams) (api.KernelEntry, error) {
|
||||||
name := strings.TrimSpace(params.Name)
|
name := strings.TrimSpace(params.Name)
|
||||||
if err := kernelcat.ValidateName(name); err != nil {
|
if err := kernelcat.ValidateName(name); err != nil {
|
||||||
return api.KernelEntry{}, err
|
return api.KernelEntry{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !params.Force {
|
if !params.Force {
|
||||||
if _, err := kernelcat.ReadLocal(d.layout.KernelsDir, name); err == nil {
|
if _, err := kernelcat.ReadLocal(s.layout.KernelsDir, name); err == nil {
|
||||||
return api.KernelEntry{}, fmt.Errorf("kernel %q already pulled; pass --force to re-pull", name)
|
return api.KernelEntry{}, fmt.Errorf("kernel %q already pulled; pass --force to re-pull", name)
|
||||||
} else if !os.IsNotExist(err) {
|
} else if !os.IsNotExist(err) {
|
||||||
return api.KernelEntry{}, err
|
return api.KernelEntry{}, err
|
||||||
|
|
@ -139,7 +139,7 @@ func (d *Daemon) KernelPull(ctx context.Context, params api.KernelPullParams) (a
|
||||||
return api.KernelEntry{}, fmt.Errorf("kernel %q not in catalog (run 'banger kernel list --available' to browse)", name)
|
return api.KernelEntry{}, fmt.Errorf("kernel %q not in catalog (run 'banger kernel list --available' to browse)", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
stored, err := kernelcat.Fetch(ctx, nil, d.layout.KernelsDir, catEntry)
|
stored, err := kernelcat.Fetch(ctx, nil, s.layout.KernelsDir, catEntry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return api.KernelEntry{}, err
|
return api.KernelEntry{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -148,12 +148,12 @@ func (d *Daemon) KernelPull(ctx context.Context, params api.KernelPullParams) (a
|
||||||
|
|
||||||
// KernelCatalog returns every entry from the embedded catalog annotated
|
// KernelCatalog returns every entry from the embedded catalog annotated
|
||||||
// with whether it has already been pulled locally.
|
// with whether it has already been pulled locally.
|
||||||
func (d *Daemon) KernelCatalog(_ context.Context) (api.KernelCatalogResult, error) {
|
func (s *ImageService) KernelCatalog(_ context.Context) (api.KernelCatalogResult, error) {
|
||||||
catalog, err := kernelcat.LoadEmbedded()
|
catalog, err := kernelcat.LoadEmbedded()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return api.KernelCatalogResult{}, err
|
return api.KernelCatalogResult{}, err
|
||||||
}
|
}
|
||||||
local, _ := kernelcat.ListLocal(d.layout.KernelsDir)
|
local, _ := kernelcat.ListLocal(s.layout.KernelsDir)
|
||||||
pulled := make(map[string]bool, len(local))
|
pulled := make(map[string]bool, len(local))
|
||||||
for _, entry := range local {
|
for _, entry := range local {
|
||||||
pulled[entry.Name] = true
|
pulled[entry.Name] = true
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ func TestKernelListReturnsSeededEntries(t *testing.T) {
|
||||||
seedKernelEntry(t, kernelsDir, "alpine-3.23")
|
seedKernelEntry(t, kernelsDir, "alpine-3.23")
|
||||||
|
|
||||||
d := &Daemon{layout: paths.Layout{KernelsDir: kernelsDir}}
|
d := &Daemon{layout: paths.Layout{KernelsDir: kernelsDir}}
|
||||||
result, err := d.KernelList(context.Background())
|
result, err := d.imageSvc().KernelList(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("KernelList: %v", err)
|
t.Fatalf("KernelList: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +86,7 @@ func TestKernelShowAndDeleteThroughDispatch(t *testing.T) {
|
||||||
|
|
||||||
func TestKernelShowMissingEntry(t *testing.T) {
|
func TestKernelShowMissingEntry(t *testing.T) {
|
||||||
d := &Daemon{layout: paths.Layout{KernelsDir: t.TempDir()}}
|
d := &Daemon{layout: paths.Layout{KernelsDir: t.TempDir()}}
|
||||||
_, err := d.KernelShow(context.Background(), "nope")
|
_, err := d.imageSvc().KernelShow(context.Background(), "nope")
|
||||||
if err == nil || !strings.Contains(err.Error(), "not found") {
|
if err == nil || !strings.Contains(err.Error(), "not found") {
|
||||||
t.Fatalf("KernelShow missing: err=%v", err)
|
t.Fatalf("KernelShow missing: err=%v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +94,7 @@ func TestKernelShowMissingEntry(t *testing.T) {
|
||||||
|
|
||||||
func TestKernelDeleteRejectsInvalidName(t *testing.T) {
|
func TestKernelDeleteRejectsInvalidName(t *testing.T) {
|
||||||
d := &Daemon{layout: paths.Layout{KernelsDir: t.TempDir()}}
|
d := &Daemon{layout: paths.Layout{KernelsDir: t.TempDir()}}
|
||||||
if err := d.KernelDelete(context.Background(), "../escape"); err == nil {
|
if err := d.imageSvc().KernelDelete(context.Background(), "../escape"); err == nil {
|
||||||
t.Fatalf("KernelDelete should reject traversal")
|
t.Fatalf("KernelDelete should reject traversal")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -113,7 +113,7 @@ func TestRegisterImageResolvesKernelRef(t *testing.T) {
|
||||||
store: openDaemonStore(t),
|
store: openDaemonStore(t),
|
||||||
}
|
}
|
||||||
|
|
||||||
image, err := d.RegisterImage(context.Background(), api.ImageRegisterParams{
|
image, err := d.imageSvc().RegisterImage(context.Background(), api.ImageRegisterParams{
|
||||||
Name: "testbox",
|
Name: "testbox",
|
||||||
RootfsPath: rootfs,
|
RootfsPath: rootfs,
|
||||||
KernelRef: "void-6.12",
|
KernelRef: "void-6.12",
|
||||||
|
|
@ -139,7 +139,7 @@ func TestRegisterImageRejectsKernelRefAndPath(t *testing.T) {
|
||||||
layout: paths.Layout{KernelsDir: kernelsDir},
|
layout: paths.Layout{KernelsDir: kernelsDir},
|
||||||
store: openDaemonStore(t),
|
store: openDaemonStore(t),
|
||||||
}
|
}
|
||||||
_, err := d.RegisterImage(context.Background(), api.ImageRegisterParams{
|
_, err := d.imageSvc().RegisterImage(context.Background(), api.ImageRegisterParams{
|
||||||
Name: "testbox",
|
Name: "testbox",
|
||||||
RootfsPath: rootfs,
|
RootfsPath: rootfs,
|
||||||
KernelRef: "void-6.12",
|
KernelRef: "void-6.12",
|
||||||
|
|
@ -175,7 +175,7 @@ func TestKernelImportCopiesArtifactsAndWritesManifest(t *testing.T) {
|
||||||
runner: system.NewRunner(),
|
runner: system.NewRunner(),
|
||||||
}
|
}
|
||||||
|
|
||||||
entry, err := d.KernelImport(context.Background(), api.KernelImportParams{
|
entry, err := d.imageSvc().KernelImport(context.Background(), api.KernelImportParams{
|
||||||
Name: "void-6.12",
|
Name: "void-6.12",
|
||||||
FromDir: src,
|
FromDir: src,
|
||||||
Distro: "void",
|
Distro: "void",
|
||||||
|
|
@ -210,7 +210,7 @@ func TestKernelPullRejectsUnknownCatalogEntry(t *testing.T) {
|
||||||
layout: paths.Layout{KernelsDir: t.TempDir()},
|
layout: paths.Layout{KernelsDir: t.TempDir()},
|
||||||
runner: system.NewRunner(),
|
runner: system.NewRunner(),
|
||||||
}
|
}
|
||||||
_, err := d.KernelPull(context.Background(), api.KernelPullParams{Name: "unknown"})
|
_, err := d.imageSvc().KernelPull(context.Background(), api.KernelPullParams{Name: "unknown"})
|
||||||
if err == nil || !strings.Contains(err.Error(), "not in catalog") {
|
if err == nil || !strings.Contains(err.Error(), "not in catalog") {
|
||||||
t.Fatalf("KernelPull unknown: err=%v", err)
|
t.Fatalf("KernelPull unknown: err=%v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -224,7 +224,7 @@ func TestKernelPullRefusesOverwriteWithoutForce(t *testing.T) {
|
||||||
layout: paths.Layout{KernelsDir: kernelsDir},
|
layout: paths.Layout{KernelsDir: kernelsDir},
|
||||||
runner: system.NewRunner(),
|
runner: system.NewRunner(),
|
||||||
}
|
}
|
||||||
_, err := d.KernelPull(context.Background(), api.KernelPullParams{Name: "void-6.12"})
|
_, err := d.imageSvc().KernelPull(context.Background(), api.KernelPullParams{Name: "void-6.12"})
|
||||||
if err == nil || !strings.Contains(err.Error(), "already pulled") {
|
if err == nil || !strings.Contains(err.Error(), "already pulled") {
|
||||||
t.Fatalf("KernelPull without --force: err=%v", err)
|
t.Fatalf("KernelPull without --force: err=%v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -232,7 +232,7 @@ func TestKernelPullRefusesOverwriteWithoutForce(t *testing.T) {
|
||||||
|
|
||||||
func TestKernelCatalogReportsPulledStatus(t *testing.T) {
|
func TestKernelCatalogReportsPulledStatus(t *testing.T) {
|
||||||
d := &Daemon{layout: paths.Layout{KernelsDir: t.TempDir()}}
|
d := &Daemon{layout: paths.Layout{KernelsDir: t.TempDir()}}
|
||||||
result, err := d.KernelCatalog(context.Background())
|
result, err := d.imageSvc().KernelCatalog(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("KernelCatalog: %v", err)
|
t.Fatalf("KernelCatalog: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -247,7 +247,7 @@ func TestKernelImportRejectsMissingFromDir(t *testing.T) {
|
||||||
layout: paths.Layout{KernelsDir: t.TempDir()},
|
layout: paths.Layout{KernelsDir: t.TempDir()},
|
||||||
runner: system.NewRunner(),
|
runner: system.NewRunner(),
|
||||||
}
|
}
|
||||||
_, err := d.KernelImport(context.Background(), api.KernelImportParams{Name: "x"})
|
_, err := d.imageSvc().KernelImport(context.Background(), api.KernelImportParams{Name: "x"})
|
||||||
if err == nil || !strings.Contains(err.Error(), "--from") {
|
if err == nil || !strings.Contains(err.Error(), "--from") {
|
||||||
t.Fatalf("KernelImport without --from: err=%v", err)
|
t.Fatalf("KernelImport without --from: err=%v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -262,7 +262,7 @@ func TestRegisterImageMissingKernelRef(t *testing.T) {
|
||||||
layout: paths.Layout{KernelsDir: t.TempDir()},
|
layout: paths.Layout{KernelsDir: t.TempDir()},
|
||||||
store: openDaemonStore(t),
|
store: openDaemonStore(t),
|
||||||
}
|
}
|
||||||
_, err := d.RegisterImage(context.Background(), api.ImageRegisterParams{
|
_, err := d.imageSvc().RegisterImage(context.Background(), api.ImageRegisterParams{
|
||||||
Name: "testbox",
|
Name: "testbox",
|
||||||
RootfsPath: rootfs,
|
RootfsPath: rootfs,
|
||||||
KernelRef: "never-imported",
|
KernelRef: "never-imported",
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ func (d *Daemon) ensureAuthorizedKeyOnWorkDisk(ctx context.Context, vm *model.VM
|
||||||
}
|
}
|
||||||
if prep.ClonedFromSeed && image.Managed {
|
if prep.ClonedFromSeed && image.Managed {
|
||||||
vmCreateStage(ctx, "prepare_work_disk", "refreshing managed work seed")
|
vmCreateStage(ctx, "prepare_work_disk", "refreshing managed work seed")
|
||||||
if err := d.refreshManagedWorkSeedFingerprint(ctx, image, fingerprint); err != nil {
|
if err := d.imageSvc().refreshManagedWorkSeedFingerprint(ctx, image, fingerprint); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ func (d *Daemon) reserveVM(ctx context.Context, requestedName string, image mode
|
||||||
// therefore `vm run`) works on a fresh host without the user having
|
// therefore `vm run`) works on a fresh host without the user having
|
||||||
// to run `image pull` first.
|
// to run `image pull` first.
|
||||||
func (d *Daemon) findOrAutoPullImage(ctx context.Context, idOrName string) (model.Image, error) {
|
func (d *Daemon) findOrAutoPullImage(ctx context.Context, idOrName string) (model.Image, error) {
|
||||||
image, err := d.FindImage(ctx, idOrName)
|
image, err := d.imageSvc().FindImage(ctx, idOrName)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return image, nil
|
return image, nil
|
||||||
}
|
}
|
||||||
|
|
@ -189,8 +189,8 @@ func (d *Daemon) findOrAutoPullImage(ctx context.Context, idOrName string) (mode
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
vmCreateStage(ctx, "auto_pull_image", fmt.Sprintf("pulling %s from image catalog", entry.Name))
|
vmCreateStage(ctx, "auto_pull_image", fmt.Sprintf("pulling %s from image catalog", entry.Name))
|
||||||
if _, pullErr := d.PullImage(ctx, api.ImagePullParams{Ref: entry.Name}); pullErr != nil {
|
if _, pullErr := d.imageSvc().PullImage(ctx, api.ImagePullParams{Ref: entry.Name}); pullErr != nil {
|
||||||
return model.Image{}, fmt.Errorf("auto-pull image %q: %w", entry.Name, pullErr)
|
return model.Image{}, fmt.Errorf("auto-pull image %q: %w", entry.Name, pullErr)
|
||||||
}
|
}
|
||||||
return d.FindImage(ctx, idOrName)
|
return d.imageSvc().FindImage(ctx, idOrName)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -190,12 +190,15 @@ func sshdGuestConfig() string {
|
||||||
}, "\n")
|
}, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Daemon) flattenNestedWorkHome(ctx context.Context, workMount string) error {
|
// flattenNestedWorkHome is a package-level helper used by the image,
|
||||||
|
// workspace-sync, and VM-disk paths, so it takes the runner explicitly
|
||||||
|
// rather than belonging to any one service struct.
|
||||||
|
func flattenNestedWorkHome(ctx context.Context, runner system.CommandRunner, workMount string) error {
|
||||||
nestedHome := filepath.Join(workMount, "root")
|
nestedHome := filepath.Join(workMount, "root")
|
||||||
if !exists(nestedHome) {
|
if !exists(nestedHome) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if _, err := d.runner.RunSudo(ctx, "chmod", "755", nestedHome); err != nil {
|
if _, err := runner.RunSudo(ctx, "chmod", "755", nestedHome); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
entries, err := os.ReadDir(nestedHome)
|
entries, err := os.ReadDir(nestedHome)
|
||||||
|
|
@ -204,10 +207,17 @@ func (d *Daemon) flattenNestedWorkHome(ctx context.Context, workMount string) er
|
||||||
}
|
}
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
sourcePath := filepath.Join(nestedHome, entry.Name())
|
sourcePath := filepath.Join(nestedHome, entry.Name())
|
||||||
if _, err := d.runner.RunSudo(ctx, "cp", "-a", sourcePath, workMount+"/"); err != nil {
|
if _, err := runner.RunSudo(ctx, "cp", "-a", sourcePath, workMount+"/"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = d.runner.RunSudo(ctx, "rm", "-rf", nestedHome)
|
_, err = runner.RunSudo(ctx, "rm", "-rf", nestedHome)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated forwarder: until every caller learns the package-level
|
||||||
|
// helper, Daemon keeps a receiver-method form. Will be deleted once
|
||||||
|
// the last caller is rewritten.
|
||||||
|
func (d *Daemon) flattenNestedWorkHome(ctx context.Context, workMount string) error {
|
||||||
|
return flattenNestedWorkHome(ctx, d.runner, workMount)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue