Remove runtime-bundle image dependencies
Hard-cut banger away from source-checkout runtime bundles as an implicit source of\nimage and host defaults. Managed images now own their full boot set,\nimage build starts from an existing registered image, and daemon startup\nno longer synthesizes a default image from host paths.\n\nResolve Firecracker from PATH or firecracker_bin, make SSH keys config-owned\nwith an auto-managed XDG default, replace the external name generator and\npackage manifests with Go code, and keep the vsock helper as a companion\nbinary instead of a user-managed runtime asset.\n\nUpdate the manual scripts, web/CLI forms, config surface, and docs around\nthe new build/manual flow and explicit image registration semantics.\n\nValidation: GOCACHE=/tmp/banger-gocache go test ./..., bash -n scripts/*.sh,\nand make build.
This commit is contained in:
parent
01c7cb5e65
commit
572bf32424
44 changed files with 1194 additions and 3456 deletions
|
|
@ -10,8 +10,8 @@ import (
|
|||
"strings"
|
||||
|
||||
"banger/internal/api"
|
||||
"banger/internal/imagepreset"
|
||||
"banger/internal/model"
|
||||
"banger/internal/paths"
|
||||
"banger/internal/system"
|
||||
)
|
||||
|
||||
|
|
@ -37,12 +37,13 @@ func (d *Daemon) BuildImage(ctx context.Context, params api.ImageBuildParams) (i
|
|||
if _, err := d.FindImage(ctx, name); err == nil {
|
||||
return model.Image{}, fmt.Errorf("image name already exists: %s", name)
|
||||
}
|
||||
baseRootfs := params.BaseRootfs
|
||||
if baseRootfs == "" {
|
||||
baseRootfs = d.config.DefaultBaseRootfs
|
||||
fromImage := strings.TrimSpace(params.FromImage)
|
||||
if fromImage == "" {
|
||||
return model.Image{}, fmt.Errorf("from-image is required")
|
||||
}
|
||||
if baseRootfs == "" {
|
||||
return model.Image{}, fmt.Errorf("base rootfs is required; %s", paths.RuntimeBundleHint())
|
||||
baseImage, err := d.FindImage(ctx, fromImage)
|
||||
if err != nil {
|
||||
return model.Image{}, err
|
||||
}
|
||||
id, err := model.NewID()
|
||||
if err != nil {
|
||||
|
|
@ -50,9 +51,6 @@ func (d *Daemon) BuildImage(ctx context.Context, params api.ImageBuildParams) (i
|
|||
}
|
||||
now := model.Now()
|
||||
artifactDir := filepath.Join(d.layout.ImagesDir, id)
|
||||
if err := os.MkdirAll(artifactDir, 0o755); err != nil {
|
||||
return model.Image{}, err
|
||||
}
|
||||
buildLogDir := filepath.Join(d.layout.StateDir, "image-build")
|
||||
if err := os.MkdirAll(buildLogDir, 0o755); err != nil {
|
||||
return model.Image{}, err
|
||||
|
|
@ -64,73 +62,80 @@ func (d *Daemon) BuildImage(ctx context.Context, params api.ImageBuildParams) (i
|
|||
return model.Image{}, err
|
||||
}
|
||||
defer logFile.Close()
|
||||
rootfsPath := filepath.Join(artifactDir, "rootfs.ext4")
|
||||
workSeedPath := filepath.Join(artifactDir, "work-seed.ext4")
|
||||
kernelPath := params.KernelPath
|
||||
if kernelPath == "" {
|
||||
kernelPath = d.config.DefaultKernel
|
||||
}
|
||||
initrdPath := params.InitrdPath
|
||||
if initrdPath == "" {
|
||||
initrdPath = d.config.DefaultInitrd
|
||||
}
|
||||
modulesDir := params.ModulesDir
|
||||
if modulesDir == "" {
|
||||
modulesDir = d.config.DefaultModulesDir
|
||||
}
|
||||
if err := d.validateImageBuildPrereqs(ctx, baseRootfs, kernelPath, initrdPath, modulesDir, params.Size); err != nil {
|
||||
stageDir, err := os.MkdirTemp(d.layout.ImagesDir, id+".build-")
|
||||
if err != nil {
|
||||
return model.Image{}, err
|
||||
}
|
||||
cleanupStage := true
|
||||
defer func() {
|
||||
if cleanupStage {
|
||||
_ = os.RemoveAll(stageDir)
|
||||
}
|
||||
}()
|
||||
rootfsPath := filepath.Join(stageDir, "rootfs.ext4")
|
||||
workSeedPath := filepath.Join(stageDir, "work-seed.ext4")
|
||||
kernelSource := firstNonEmpty(params.KernelPath, baseImage.KernelPath)
|
||||
initrdSource := firstNonEmpty(params.InitrdPath, baseImage.InitrdPath)
|
||||
modulesSource := firstNonEmpty(params.ModulesDir, baseImage.ModulesDir)
|
||||
if err := d.validateImageBuildPrereqs(ctx, baseImage.RootfsPath, kernelSource, initrdSource, modulesSource, params.Size); err != nil {
|
||||
return model.Image{}, err
|
||||
}
|
||||
kernelPath, initrdPath, modulesDir, err := stageManagedBootArtifacts(ctx, d.runner, stageDir, kernelSource, initrdSource, modulesSource)
|
||||
if err != nil {
|
||||
return model.Image{}, err
|
||||
}
|
||||
packages := imagepreset.DebianBasePackages()
|
||||
metadataPackages := imageBuildMetadataPackages(params.Docker)
|
||||
spec := imageBuildSpec{
|
||||
ID: id,
|
||||
Name: name,
|
||||
BaseRootfs: baseRootfs,
|
||||
SourceRootfs: baseImage.RootfsPath,
|
||||
RootfsPath: rootfsPath,
|
||||
BuildLog: logFile,
|
||||
KernelPath: kernelPath,
|
||||
InitrdPath: initrdPath,
|
||||
ModulesDir: modulesDir,
|
||||
PackagesPath: d.config.DefaultPackagesFile,
|
||||
Packages: packages,
|
||||
InstallDocker: params.Docker,
|
||||
Size: params.Size,
|
||||
}
|
||||
op.stage("launch_builder", "build_log_path", buildLogPath, "artifact_dir", artifactDir)
|
||||
op.stage("launch_builder", "build_log_path", buildLogPath, "artifact_dir", artifactDir, "from_image", baseImage.Name)
|
||||
imageBuildStage(ctx, "launch_builder", "building rootfs from base image")
|
||||
if err := d.runImageBuild(ctx, spec); err != nil {
|
||||
_ = logFile.Sync()
|
||||
_ = os.RemoveAll(artifactDir)
|
||||
return model.Image{}, err
|
||||
}
|
||||
imageBuildStage(ctx, "prepare_work_seed", "building reusable work seed")
|
||||
if err := system.BuildWorkSeedImage(ctx, d.runner, rootfsPath, workSeedPath); err != nil {
|
||||
_ = logFile.Sync()
|
||||
_ = os.RemoveAll(artifactDir)
|
||||
return model.Image{}, err
|
||||
}
|
||||
imageBuildStage(ctx, "seed_ssh", "seeding runtime SSH access")
|
||||
seededSSHPublicKeyFingerprint, err := d.seedAuthorizedKeyOnExt4Image(ctx, workSeedPath)
|
||||
if err != nil {
|
||||
_ = logFile.Sync()
|
||||
_ = os.RemoveAll(artifactDir)
|
||||
return model.Image{}, err
|
||||
}
|
||||
imageBuildStage(ctx, "write_metadata", "writing image metadata")
|
||||
if err := writePackagesMetadata(rootfsPath, d.config.DefaultPackagesFile); err != nil {
|
||||
if err := writePackagesMetadata(rootfsPath, metadataPackages); err != nil {
|
||||
_ = logFile.Sync()
|
||||
_ = os.RemoveAll(artifactDir)
|
||||
return model.Image{}, err
|
||||
}
|
||||
op.stage("activate_artifacts", "artifact_dir", artifactDir)
|
||||
if err := os.Rename(stageDir, artifactDir); err != nil {
|
||||
return model.Image{}, err
|
||||
}
|
||||
cleanupStage = false
|
||||
image = model.Image{
|
||||
ID: id,
|
||||
Name: name,
|
||||
Managed: true,
|
||||
ArtifactDir: artifactDir,
|
||||
RootfsPath: rootfsPath,
|
||||
WorkSeedPath: workSeedPath,
|
||||
KernelPath: kernelPath,
|
||||
InitrdPath: initrdPath,
|
||||
ModulesDir: modulesDir,
|
||||
PackagesPath: d.config.DefaultPackagesFile,
|
||||
RootfsPath: filepath.Join(artifactDir, "rootfs.ext4"),
|
||||
WorkSeedPath: filepath.Join(artifactDir, "work-seed.ext4"),
|
||||
KernelPath: filepath.Join(artifactDir, "kernel"),
|
||||
InitrdPath: stageOptionalArtifactPath(artifactDir, initrdPath, "initrd.img"),
|
||||
ModulesDir: stageOptionalArtifactPath(artifactDir, modulesDir, "modules"),
|
||||
BuildSize: params.Size,
|
||||
SeededSSHPublicKeyFingerprint: seededSSHPublicKeyFingerprint,
|
||||
Docker: params.Docker,
|
||||
|
|
@ -174,19 +179,12 @@ func (d *Daemon) RegisterImage(ctx context.Context, params api.ImageRegisterPara
|
|||
}
|
||||
kernelPath := strings.TrimSpace(params.KernelPath)
|
||||
if kernelPath == "" {
|
||||
kernelPath = d.config.DefaultKernel
|
||||
return model.Image{}, fmt.Errorf("kernel path is required")
|
||||
}
|
||||
initrdPath := strings.TrimSpace(params.InitrdPath)
|
||||
if initrdPath == "" {
|
||||
initrdPath = d.config.DefaultInitrd
|
||||
}
|
||||
modulesDir := strings.TrimSpace(params.ModulesDir)
|
||||
if modulesDir == "" {
|
||||
modulesDir = d.config.DefaultModulesDir
|
||||
}
|
||||
packagesPath := strings.TrimSpace(params.PackagesPath)
|
||||
|
||||
if err := validateImageRegisterPaths(rootfsPath, workSeedPath, kernelPath, initrdPath, modulesDir, packagesPath); err != nil {
|
||||
if err := validateImageRegisterPaths(rootfsPath, workSeedPath, kernelPath, initrdPath, modulesDir); err != nil {
|
||||
return model.Image{}, err
|
||||
}
|
||||
|
||||
|
|
@ -203,7 +201,6 @@ func (d *Daemon) RegisterImage(ctx context.Context, params api.ImageRegisterPara
|
|||
image.KernelPath = kernelPath
|
||||
image.InitrdPath = initrdPath
|
||||
image.ModulesDir = modulesDir
|
||||
image.PackagesPath = packagesPath
|
||||
image.Docker = params.Docker
|
||||
image.UpdatedAt = now
|
||||
case errors.Is(lookupErr, sql.ErrNoRows):
|
||||
|
|
@ -220,7 +217,6 @@ func (d *Daemon) RegisterImage(ctx context.Context, params api.ImageRegisterPara
|
|||
KernelPath: kernelPath,
|
||||
InitrdPath: initrdPath,
|
||||
ModulesDir: modulesDir,
|
||||
PackagesPath: packagesPath,
|
||||
Docker: params.Docker,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
|
|
@ -255,7 +251,7 @@ func (d *Daemon) PromoteImage(ctx context.Context, idOrName string) (image model
|
|||
if image.Managed {
|
||||
return model.Image{}, fmt.Errorf("image %s is already managed", image.Name)
|
||||
}
|
||||
if err := validateImagePromotePaths(image.RootfsPath, image.KernelPath, image.InitrdPath, image.ModulesDir, image.PackagesPath); err != nil {
|
||||
if err := validateImagePromotePaths(image.RootfsPath, image.KernelPath, image.InitrdPath, image.ModulesDir); err != nil {
|
||||
return model.Image{}, err
|
||||
}
|
||||
if strings.TrimSpace(d.layout.ImagesDir) == "" {
|
||||
|
|
@ -313,6 +309,10 @@ func (d *Daemon) PromoteImage(ctx context.Context, idOrName string) (image model
|
|||
} else {
|
||||
image.SeededSSHPublicKeyFingerprint = ""
|
||||
}
|
||||
_, initrdPath, modulesDir, err := stageManagedBootArtifacts(ctx, d.runner, stageDir, image.KernelPath, image.InitrdPath, image.ModulesDir)
|
||||
if err != nil {
|
||||
return model.Image{}, err
|
||||
}
|
||||
|
||||
op.stage("activate_artifacts", "artifact_dir", artifactDir)
|
||||
if err := os.Rename(stageDir, artifactDir); err != nil {
|
||||
|
|
@ -326,6 +326,9 @@ func (d *Daemon) PromoteImage(ctx context.Context, idOrName string) (image model
|
|||
if workSeedPath != "" {
|
||||
image.WorkSeedPath = filepath.Join(artifactDir, "work-seed.ext4")
|
||||
}
|
||||
image.KernelPath = filepath.Join(artifactDir, "kernel")
|
||||
image.InitrdPath = stageOptionalArtifactPath(artifactDir, initrdPath, "initrd.img")
|
||||
image.ModulesDir = stageOptionalArtifactPath(artifactDir, modulesDir, "modules")
|
||||
image.UpdatedAt = model.Now()
|
||||
if err := d.store.UpsertImage(ctx, image); err != nil {
|
||||
_ = os.RemoveAll(artifactDir)
|
||||
|
|
@ -334,26 +337,23 @@ func (d *Daemon) PromoteImage(ctx context.Context, idOrName string) (image model
|
|||
return image, nil
|
||||
}
|
||||
|
||||
func validateImageRegisterPaths(rootfsPath, workSeedPath, kernelPath, initrdPath, modulesDir, packagesPath string) error {
|
||||
func validateImageRegisterPaths(rootfsPath, workSeedPath, kernelPath, initrdPath, modulesDir string) error {
|
||||
checks := system.NewPreflight()
|
||||
checks.RequireFile(rootfsPath, "rootfs image", `pass --rootfs <path>`)
|
||||
checks.RequireFile(kernelPath, "kernel image", `pass --kernel <path> or set "default_kernel"`)
|
||||
checks.RequireFile(kernelPath, "kernel image", `pass --kernel <path>`)
|
||||
if workSeedPath != "" {
|
||||
checks.RequireFile(workSeedPath, "work-seed image", `pass --work-seed <path> or rebuild the image with a work seed`)
|
||||
}
|
||||
if initrdPath != "" {
|
||||
checks.RequireFile(initrdPath, "initrd image", `pass --initrd <path> or set "default_initrd"`)
|
||||
checks.RequireFile(initrdPath, "initrd image", `pass --initrd <path>`)
|
||||
}
|
||||
if modulesDir != "" {
|
||||
checks.RequireDir(modulesDir, "kernel modules dir", `pass --modules <dir> or set "default_modules_dir"`)
|
||||
}
|
||||
if packagesPath != "" {
|
||||
checks.RequireFile(packagesPath, "packages manifest", `pass --packages <path>`)
|
||||
checks.RequireDir(modulesDir, "kernel modules dir", `pass --modules <dir>`)
|
||||
}
|
||||
return checks.Err("image register failed")
|
||||
}
|
||||
|
||||
func validateImagePromotePaths(rootfsPath, kernelPath, initrdPath, modulesDir, packagesPath string) error {
|
||||
func validateImagePromotePaths(rootfsPath, kernelPath, initrdPath, modulesDir string) error {
|
||||
checks := system.NewPreflight()
|
||||
checks.RequireFile(rootfsPath, "rootfs image", `re-register the image with a valid rootfs`)
|
||||
checks.RequireFile(kernelPath, "kernel image", `re-register the image with a valid kernel`)
|
||||
|
|
@ -363,22 +363,15 @@ func validateImagePromotePaths(rootfsPath, kernelPath, initrdPath, modulesDir, p
|
|||
if modulesDir != "" {
|
||||
checks.RequireDir(modulesDir, "kernel modules dir", `re-register the image with a valid modules dir`)
|
||||
}
|
||||
if packagesPath != "" {
|
||||
checks.RequireFile(packagesPath, "packages manifest", `re-register the image with a valid packages manifest`)
|
||||
}
|
||||
return checks.Err("image promote failed")
|
||||
}
|
||||
|
||||
func writePackagesMetadata(rootfsPath, packagesPath string) error {
|
||||
if rootfsPath == "" || packagesPath == "" {
|
||||
func writePackagesMetadata(rootfsPath string, packages []string) error {
|
||||
if rootfsPath == "" || len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
lines, err := system.ReadNormalizedLines(packagesPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
metadataPath := rootfsPath + ".packages.sha256"
|
||||
return os.WriteFile(metadataPath, []byte(packagesHash(lines)+"\n"), 0o644)
|
||||
return os.WriteFile(metadataPath, []byte(packagesHash(packages)+"\n"), 0o644)
|
||||
}
|
||||
|
||||
func (d *Daemon) DeleteImage(ctx context.Context, idOrName string) (model.Image, error) {
|
||||
|
|
@ -406,3 +399,52 @@ func (d *Daemon) DeleteImage(ctx context.Context, idOrName string) (model.Image,
|
|||
}
|
||||
return image, nil
|
||||
}
|
||||
|
||||
func stageManagedBootArtifacts(ctx context.Context, runner system.CommandRunner, artifactDir, kernelSource, initrdSource, modulesSource string) (string, string, string, error) {
|
||||
kernelPath := filepath.Join(artifactDir, "kernel")
|
||||
if err := system.CopyFilePreferClone(kernelSource, kernelPath); err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
initrdPath := ""
|
||||
if strings.TrimSpace(initrdSource) != "" {
|
||||
initrdPath = filepath.Join(artifactDir, "initrd.img")
|
||||
if err := system.CopyFilePreferClone(initrdSource, initrdPath); err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
}
|
||||
modulesDir := ""
|
||||
if strings.TrimSpace(modulesSource) != "" {
|
||||
modulesDir = filepath.Join(artifactDir, "modules")
|
||||
if err := os.MkdirAll(modulesDir, 0o755); err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
if err := system.CopyDirContents(ctx, runner, modulesSource, modulesDir, false); err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
}
|
||||
return kernelPath, initrdPath, modulesDir, nil
|
||||
}
|
||||
|
||||
func imageBuildMetadataPackages(docker bool) []string {
|
||||
packages := imagepreset.DebianBasePackages()
|
||||
if docker {
|
||||
packages = append(packages, "#feature:docker")
|
||||
}
|
||||
return packages
|
||||
}
|
||||
|
||||
func stageOptionalArtifactPath(artifactDir, stagedPath, name string) string {
|
||||
if strings.TrimSpace(stagedPath) == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(artifactDir, name)
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue