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>
96 lines
2.8 KiB
Go
96 lines
2.8 KiB
Go
package daemon
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"banger/internal/guest"
|
|
"banger/internal/model"
|
|
"banger/internal/system"
|
|
)
|
|
|
|
func (s *ImageService) seedAuthorizedKeyOnExt4Image(ctx context.Context, imagePath string) (string, error) {
|
|
if strings.TrimSpace(s.config.SSHKeyPath) == "" {
|
|
return "", nil
|
|
}
|
|
fingerprint, err := guest.AuthorizedPublicKeyFingerprint(s.config.SSHKeyPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("derive authorized ssh key fingerprint: %w", err)
|
|
}
|
|
publicKey, err := guest.AuthorizedPublicKey(s.config.SSHKeyPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("derive authorized ssh key: %w", err)
|
|
}
|
|
mountDir, cleanup, err := system.MountTempDir(ctx, s.runner, imagePath, false)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer cleanup()
|
|
|
|
if err := flattenNestedWorkHome(ctx, s.runner, mountDir); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Same rationale as in ensureAuthorizedKeyOnWorkDisk — the seed's
|
|
// filesystem root becomes /root inside the guest, and sshd's
|
|
// StrictModes check walks its ownership and mode.
|
|
if err := normaliseHomeDirPerms(ctx, s.runner, mountDir); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
sshDir := filepath.Join(mountDir, ".ssh")
|
|
if _, err := s.runner.RunSudo(ctx, "mkdir", "-p", sshDir); err != nil {
|
|
return "", err
|
|
}
|
|
if _, err := s.runner.RunSudo(ctx, "chmod", "700", sshDir); err != nil {
|
|
return "", err
|
|
}
|
|
if _, err := s.runner.RunSudo(ctx, "chown", "0:0", sshDir); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
authorizedKeysPath := filepath.Join(sshDir, "authorized_keys")
|
|
existing, err := s.runner.RunSudo(ctx, "cat", authorizedKeysPath)
|
|
if err != nil {
|
|
existing = nil
|
|
}
|
|
merged := mergeAuthorizedKey(existing, publicKey)
|
|
tmpFile, err := os.CreateTemp("", "banger-image-authorized-keys-*")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
tmpPath := tmpFile.Name()
|
|
if _, err := tmpFile.Write(merged); err != nil {
|
|
_ = tmpFile.Close()
|
|
_ = os.Remove(tmpPath)
|
|
return "", err
|
|
}
|
|
if err := tmpFile.Close(); err != nil {
|
|
_ = os.Remove(tmpPath)
|
|
return "", err
|
|
}
|
|
defer os.Remove(tmpPath)
|
|
if _, err := s.runner.RunSudo(ctx, "install", "-m", "600", tmpPath, authorizedKeysPath); err != nil {
|
|
return "", err
|
|
}
|
|
return fingerprint, nil
|
|
}
|
|
|
|
func (s *ImageService) refreshManagedWorkSeedFingerprint(ctx context.Context, image model.Image, fingerprint string) error {
|
|
if !image.Managed || strings.TrimSpace(image.WorkSeedPath) == "" || strings.TrimSpace(fingerprint) == "" {
|
|
return nil
|
|
}
|
|
seededFingerprint, err := s.seedAuthorizedKeyOnExt4Image(ctx, image.WorkSeedPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if seededFingerprint == "" || seededFingerprint == image.SeededSSHPublicKeyFingerprint {
|
|
return nil
|
|
}
|
|
image.SeededSSHPublicKeyFingerprint = seededFingerprint
|
|
image.UpdatedAt = model.Now()
|
|
return s.store.UpsertImage(ctx, image)
|
|
}
|