Remove image build --from-image; doctor treats catalog images as OK
The `image build` flow spun up a transient Firecracker VM, SSHed in, and ran a large bash provisioning script to derive a new managed image from an existing one. It overlapped heavily with the golden- image Dockerfile flow (same mise/docker/tmux/opencode install logic duplicated in Go as `imagemgr.BuildProvisionScript`) and had far more machinery: async op state, RPC begin/status/cancel, webui form + operation page, preflight checks, API types, tests. For custom images, writing a Dockerfile is simpler and more reproducible. Removed end-to-end: - CLI `image build` subcommand + `absolutizeImageBuildPaths`. - Daemon: BuildImage method, imagebuild.go (transient-VM orchestration), image_build_ops.go (async begin/status/cancel), imagemgr/build.go (the 247-line provisioning script generator and all its append* helpers), validateImageBuildPrereqs + addImageBuildPrereqs. - RPC dispatches for image.build / .begin / .status / .cancel. - opstate registry `imageBuildOps`, daemon seam `imageBuild`, background pruner call. - API types: ImageBuildParams, ImageBuildOperation, ImageBuildBeginResult, ImageBuildStatusParams, ImageBuildStatusResult; model type ImageBuildRequest. - Web UI: Backend interface methods, handlers, form, routes, template branches (images.html build form, operation.html build branch, dashboard.html Build button). - Tests that directly exercised BuildImage. Doctor polish (task C): - Drop the "image build" preflight section entirely (its raison d'être is gone). - Default-image check now accepts "not local but in imagecat" as OK: vm create auto-pulls on first use. Only flag when the image is neither locally registered nor in the catalog. Net: 24 files touched, 1,373 lines deleted, 25 added. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ace4782fce
commit
ac7974f5b9
24 changed files with 25 additions and 1398 deletions
|
|
@ -16,146 +16,6 @@ import (
|
|||
"banger/internal/system"
|
||||
)
|
||||
|
||||
func (d *Daemon) BuildImage(ctx context.Context, params api.ImageBuildParams) (image model.Image, err error) {
|
||||
d.imageOpsMu.Lock()
|
||||
defer d.imageOpsMu.Unlock()
|
||||
op := d.beginOperation("image.build")
|
||||
buildLogPath := ""
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = annotateLogPath(err, buildLogPath)
|
||||
op.fail(err, imageLogAttrs(image)...)
|
||||
return
|
||||
}
|
||||
op.done(imageLogAttrs(image)...)
|
||||
}()
|
||||
|
||||
name := params.Name
|
||||
imageBuildStage(ctx, "resolve_image", "resolving image build inputs")
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("image-%d", model.Now().Unix())
|
||||
}
|
||||
if _, err := d.FindImage(ctx, name); err == nil {
|
||||
return model.Image{}, fmt.Errorf("image name already exists: %s", name)
|
||||
}
|
||||
fromImage := strings.TrimSpace(params.FromImage)
|
||||
if fromImage == "" {
|
||||
return model.Image{}, fmt.Errorf("from-image is required")
|
||||
}
|
||||
baseImage, err := d.FindImage(ctx, fromImage)
|
||||
if err != nil {
|
||||
return model.Image{}, err
|
||||
}
|
||||
id, err := model.NewID()
|
||||
if err != nil {
|
||||
return model.Image{}, err
|
||||
}
|
||||
now := model.Now()
|
||||
artifactDir := filepath.Join(d.layout.ImagesDir, id)
|
||||
buildLogDir := filepath.Join(d.layout.StateDir, "image-build")
|
||||
if err := os.MkdirAll(buildLogDir, 0o755); err != nil {
|
||||
return model.Image{}, err
|
||||
}
|
||||
buildLogPath = filepath.Join(buildLogDir, id+".log")
|
||||
imageBuildSetLogPath(ctx, buildLogPath)
|
||||
logFile, err := os.OpenFile(buildLogPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||
if err != nil {
|
||||
return model.Image{}, err
|
||||
}
|
||||
defer logFile.Close()
|
||||
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 := imagemgr.StageBootArtifacts(ctx, d.runner, stageDir, kernelSource, initrdSource, modulesSource)
|
||||
if err != nil {
|
||||
return model.Image{}, err
|
||||
}
|
||||
packages := imagemgr.DebianBasePackages()
|
||||
metadataPackages := imagemgr.BuildMetadataPackages(params.Docker)
|
||||
spec := imageBuildSpec{
|
||||
ID: id,
|
||||
Name: name,
|
||||
SourceRootfs: baseImage.RootfsPath,
|
||||
RootfsPath: rootfsPath,
|
||||
BuildLog: logFile,
|
||||
KernelPath: kernelPath,
|
||||
InitrdPath: initrdPath,
|
||||
ModulesDir: modulesDir,
|
||||
Packages: packages,
|
||||
InstallDocker: params.Docker,
|
||||
Size: params.Size,
|
||||
}
|
||||
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()
|
||||
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()
|
||||
return model.Image{}, err
|
||||
}
|
||||
imageBuildStage(ctx, "seed_ssh", "seeding runtime SSH access")
|
||||
seededSSHPublicKeyFingerprint, err := d.seedAuthorizedKeyOnExt4Image(ctx, workSeedPath)
|
||||
if err != nil {
|
||||
_ = logFile.Sync()
|
||||
return model.Image{}, err
|
||||
}
|
||||
imageBuildStage(ctx, "write_metadata", "writing image metadata")
|
||||
if err := imagemgr.WritePackagesMetadata(rootfsPath, metadataPackages); err != nil {
|
||||
_ = logFile.Sync()
|
||||
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: filepath.Join(artifactDir, "rootfs.ext4"),
|
||||
WorkSeedPath: filepath.Join(artifactDir, "work-seed.ext4"),
|
||||
KernelPath: filepath.Join(artifactDir, "kernel"),
|
||||
InitrdPath: imagemgr.StageOptionalArtifactPath(artifactDir, initrdPath, "initrd.img"),
|
||||
ModulesDir: imagemgr.StageOptionalArtifactPath(artifactDir, modulesDir, "modules"),
|
||||
BuildSize: params.Size,
|
||||
SeededSSHPublicKeyFingerprint: seededSSHPublicKeyFingerprint,
|
||||
Docker: params.Docker,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
imageBuildBindImage(ctx, image)
|
||||
if err := d.store.UpsertImage(ctx, image); err != nil {
|
||||
return model.Image{}, err
|
||||
}
|
||||
op.stage("persisted", "build_log_path", buildLogPath)
|
||||
imageBuildStage(ctx, "persisted", "image metadata saved")
|
||||
if d.logger != nil {
|
||||
d.logger.Info("image build log preserved", append(imageLogAttrs(image), "build_log_path", buildLogPath)...)
|
||||
}
|
||||
_ = logFile.Sync()
|
||||
return image, nil
|
||||
}
|
||||
|
||||
func (d *Daemon) RegisterImage(ctx context.Context, params api.ImageRegisterParams) (image model.Image, err error) {
|
||||
d.imageOpsMu.Lock()
|
||||
defer d.imageOpsMu.Unlock()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue