From c13c8b11af916c9111220de44f4f2c7540bf2e70 Mon Sep 17 00:00:00 2001 From: Thales Maciel Date: Wed, 15 Apr 2026 16:24:22 -0300 Subject: [PATCH] Extract imagemgr subpackage with pure image helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the stateless helpers of the image subsystem into internal/daemon/imagemgr: paths.go — path validators (ValidateRegisterPaths, ValidatePromotePaths), artifact staging (StageBootArtifacts, StageOptionalArtifactPath), metadata (BuildMetadataPackages, WritePackagesMetadata). build.go — ResizeRootfs, WriteBuildLog, and the full guest provisioning script generator (BuildProvisionScript, BuildModulesCommand and all private script-append helpers) along with the mise/tmux/opencode version constants. The orchestrator methods (BuildImage, RegisterImage, PromoteImage, DeleteImage, runImageBuildNative) stay on *Daemon: they still touch d.store, d.imageOpsMu, d.beginOperation, capability hooks, and fcproc-wrapped Daemon helpers — extracting them needs prerequisite phases (operation protocol, workdisk helpers, tap pool). This commit is strictly the pure-helper extraction that can land cleanly today. imagebuild.go shrinks from 453 -> 225 LOC (half gone). images.go shrinks from 450 -> 374 LOC. imagebuild_test.go updated to call the exported imagemgr.BuildProvisionScript. Zero behavior change; all tests green. Co-Authored-By: Claude Sonnet 4.6 --- internal/daemon/imagebuild.go | 246 ++-------------------------- internal/daemon/imagebuild_test.go | 6 +- internal/daemon/imagemgr/build.go | 248 +++++++++++++++++++++++++++++ internal/daemon/imagemgr/paths.go | 108 +++++++++++++ internal/daemon/images.go | 98 ++---------- 5 files changed, 380 insertions(+), 326 deletions(-) create mode 100644 internal/daemon/imagemgr/build.go create mode 100644 internal/daemon/imagemgr/paths.go diff --git a/internal/daemon/imagebuild.go b/internal/daemon/imagebuild.go index 359892f..d248205 100644 --- a/internal/daemon/imagebuild.go +++ b/internal/daemon/imagebuild.go @@ -1,42 +1,22 @@ package daemon import ( - "bytes" "context" "errors" "fmt" "io" "os" "path/filepath" - "strings" "time" + "banger/internal/daemon/imagemgr" "banger/internal/firecracker" "banger/internal/guest" - "banger/internal/guestnet" "banger/internal/hostnat" - "banger/internal/imagepreset" "banger/internal/model" - "banger/internal/opencode" "banger/internal/system" "banger/internal/vsockagent" -) - -const ( - defaultMiseVersion = "v2025.12.0" - defaultMiseInstallPath = "/usr/local/bin/mise" - defaultMiseActivateLine = `eval "$(/usr/local/bin/mise activate bash)"` - defaultNodeTool = "node@22" - defaultOpenCodeTool = "github:anomalyco/opencode" - defaultClaudeCodeTool = "npm:@anthropic-ai/claude-code" - defaultPiTool = "npm:@mariozechner/pi-coding-agent" - defaultTPMRepo = "https://github.com/tmux-plugins/tpm" - defaultResurrectRepo = "https://github.com/tmux-plugins/tmux-resurrect" - defaultContinuumRepo = "https://github.com/tmux-plugins/tmux-continuum" - defaultTMUXPluginDir = "/root/.tmux/plugins" - defaultTMUXResurrectDir = "/root/.tmux/resurrect" - tmuxManagedBlockStart = "# >>> banger tmux plugins >>>" - tmuxManagedBlockEnd = "# <<< banger tmux plugins <<<" + "strings" ) type imageBuildSpec struct { @@ -73,7 +53,7 @@ func (d *Daemon) runImageBuildNative(ctx context.Context, spec imageBuildSpec) ( return err } if spec.Size != "" { - if err := resizeRootfs(spec.SourceRootfs, spec.RootfsPath, spec.Size); err != nil { + if err := imagemgr.ResizeRootfs(spec.SourceRootfs, spec.RootfsPath, spec.Size); err != nil { return err } } @@ -117,27 +97,27 @@ func (d *Daemon) runImageBuildNative(ctx context.Context, spec imageBuildSpec) ( if err != nil { return err } - if err := writeBuildLog(spec.BuildLog, "installing vsock agent"); err != nil { + if err := imagemgr.WriteBuildLog(spec.BuildLog, "installing vsock agent"); err != nil { return err } if err := client.UploadFile(ctx, vsockagent.GuestInstallPath, 0o755, helperBytes, spec.BuildLog); err != nil { return err } - if err := writeBuildLog(spec.BuildLog, "configuring guest"); err != nil { + if err := imagemgr.WriteBuildLog(spec.BuildLog, "configuring guest"); err != nil { return err } - if err := client.RunScript(ctx, buildProvisionScript(vm.Name, d.config.DefaultDNS, string(authorizedKey), spec.Packages, spec.InstallDocker), spec.BuildLog); err != nil { + if err := client.RunScript(ctx, imagemgr.BuildProvisionScript(vm.Name, d.config.DefaultDNS, string(authorizedKey), spec.Packages, spec.InstallDocker), spec.BuildLog); err != nil { return err } if strings.TrimSpace(spec.ModulesDir) != "" { - if err := writeBuildLog(spec.BuildLog, "copying kernel modules"); err != nil { + if err := imagemgr.WriteBuildLog(spec.BuildLog, "copying kernel modules"); err != nil { return err } - if err := client.StreamTar(ctx, spec.ModulesDir, buildModulesCommand(filepath.Base(spec.ModulesDir)), spec.BuildLog); err != nil { + if err := client.StreamTar(ctx, spec.ModulesDir, imagemgr.BuildModulesCommand(filepath.Base(spec.ModulesDir)), spec.BuildLog); err != nil { return err } } - if err := writeBuildLog(spec.BuildLog, "shutting down guest"); err != nil { + if err := imagemgr.WriteBuildLog(spec.BuildLog, "shutting down guest"); err != nil { return err } if err := client.RunScript(ctx, "set -e\nsync\n", spec.BuildLog); err != nil { @@ -146,21 +126,6 @@ func (d *Daemon) runImageBuildNative(ctx context.Context, spec imageBuildSpec) ( return d.shutdownImageBuildVM(ctx, vm) } -func resizeRootfs(baseRootfs, rootfsPath, sizeSpec string) error { - sizeBytes, err := model.ParseSize(sizeSpec) - if err != nil { - return err - } - info, err := os.Stat(baseRootfs) - if err != nil { - return err - } - if sizeBytes < info.Size() { - return fmt.Errorf("size must be >= base image size") - } - return system.ResizeExt4Image(context.Background(), system.NewRunner(), rootfsPath, sizeBytes) -} - func (d *Daemon) startImageBuildVM(ctx context.Context, spec imageBuildSpec) (imageBuildVM, func(context.Context) error, error) { if err := d.ensureBridge(ctx); err != nil { return imageBuildVM{}, nil, err @@ -258,196 +223,3 @@ func (d *Daemon) shutdownImageBuildVM(ctx context.Context, vm imageBuildVM) erro } return d.waitForExit(ctx, vm.PID, vm.APISock, 15*time.Second) } - -func buildProvisionScript(vmName, dnsServer, authorizedKey string, packages []string, installDocker bool) string { - var script bytes.Buffer - script.WriteString("set -euo pipefail\n") - fmt.Fprintf(&script, "printf 'nameserver %%s\\n' %s > /etc/resolv.conf\n", shellQuote(dnsServer)) - fmt.Fprintf(&script, "printf '%%s\\n' %s > /etc/hostname\n", shellQuote(vmName)) - fmt.Fprintf(&script, "printf '127.0.0.1 localhost\\n127.0.1.1 %%s\\n' %s > /etc/hosts\n", shellQuote(vmName)) - script.WriteString("touch /etc/fstab\n") - script.WriteString("sed -i '\\|^/dev/vdb[[:space:]]\\+/home[[:space:]]|d; \\|^/dev/vdc[[:space:]]\\+/var[[:space:]]|d' /etc/fstab\n") - script.WriteString("if ! grep -q '^tmpfs /run ' /etc/fstab; then echo 'tmpfs /run tmpfs defaults,nodev,nosuid,mode=0755 0 0' >> /etc/fstab; fi\n") - script.WriteString("if ! grep -q '^tmpfs /tmp ' /etc/fstab; then echo 'tmpfs /tmp tmpfs defaults,nodev,nosuid,mode=1777 0 0' >> /etc/fstab; fi\n") - appendAuthorizedKeySetup(&script, authorizedKey) - script.WriteString("apt-get update\n") - script.WriteString("DEBIAN_FRONTEND=noninteractive apt-get -y upgrade\n") - fmt.Fprintf(&script, "PACKAGES=%s\n", shellArray(packages)) - script.WriteString("DEBIAN_FRONTEND=noninteractive apt-get -y install \"${PACKAGES[@]}\"\n") - appendGuestNetworkSetup(&script) - appendMiseSetup(&script) - appendOpenCodeServiceSetup(&script) - appendTmuxSetup(&script) - appendVSockPingSetup(&script) - if installDocker { - script.WriteString("DEBIAN_FRONTEND=noninteractive apt-get -y remove containerd || true\n") - script.WriteString("if ! DEBIAN_FRONTEND=noninteractive apt-get -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; then\n") - script.WriteString(" DEBIAN_FRONTEND=noninteractive apt-get -y install docker.io\n") - script.WriteString("fi\n") - script.WriteString("if command -v systemctl >/dev/null 2>&1; then systemctl enable --now docker || true; fi\n") - } - appendGuestCleanup(&script) - script.WriteString("git config --system init.defaultBranch main\n") - return script.String() -} - -func appendAuthorizedKeySetup(script *bytes.Buffer, authorizedKey string) { - script.WriteString("mkdir -p /root/.ssh\n") - script.WriteString("chmod 700 /root/.ssh\n") - script.WriteString("cat > /root/.ssh/authorized_keys <<'EOF'\n") - script.WriteString(strings.TrimSpace(authorizedKey)) - script.WriteString("\nEOF\n") - script.WriteString("chmod 600 /root/.ssh/authorized_keys\n") -} - -func buildModulesCommand(modulesBase string) string { - return fmt.Sprintf("bash -se <<'EOF'\nset -euo pipefail\nmkdir -p /lib/modules\ntar -C /lib/modules -xf -\ndepmod -a %s\nmkdir -p /etc/modules-load.d\nprintf 'nf_tables\\nnft_chain_nat\\nveth\\nbr_netfilter\\noverlay\\n' > /etc/modules-load.d/docker-netfilter.conf\nmkdir -p /etc/sysctl.d\ncat > /etc/sysctl.d/99-docker.conf <<'SYSCTL'\nnet.bridge.bridge-nf-call-iptables = 1\nnet.bridge.bridge-nf-call-ip6tables = 1\nnet.ipv4.ip_forward = 1\nSYSCTL\nsysctl --system >/dev/null 2>&1 || true\nEOF", shellQuote(modulesBase)) -} - -func appendMiseSetup(script *bytes.Buffer) { - const ( - nodeShimPath = "/root/.local/share/mise/shims/node" - npmShimPath = "/root/.local/share/mise/shims/npm" - claudeShimPath = "/root/.local/share/mise/shims/claude" - piShimPath = "/root/.local/share/mise/shims/pi" - ) - - fmt.Fprintf(script, "curl -fsSL https://mise.run | MISE_INSTALL_PATH=%s MISE_VERSION=%s sh\n", shellQuote(defaultMiseInstallPath), shellQuote(defaultMiseVersion)) - fmt.Fprintf(script, "%s use -g %s\n", shellQuote(defaultMiseInstallPath), shellQuote(defaultNodeTool)) - fmt.Fprintf(script, "%s use -g %s\n", shellQuote(defaultMiseInstallPath), shellQuote(defaultOpenCodeTool)) - fmt.Fprintf(script, "%s use -g %s\n", shellQuote(defaultMiseInstallPath), shellQuote(defaultClaudeCodeTool)) - fmt.Fprintf(script, "%s use -g %s\n", shellQuote(defaultMiseInstallPath), shellQuote(defaultPiTool)) - fmt.Fprintf(script, "%s reshim\n", shellQuote(defaultMiseInstallPath)) - fmt.Fprintf(script, "if [[ ! -e %s ]]; then echo 'node shim not found after mise install' >&2; exit 1; fi\n", shellQuote(nodeShimPath)) - fmt.Fprintf(script, "if [[ ! -e %s ]]; then echo 'npm shim not found after mise install' >&2; exit 1; fi\n", shellQuote(npmShimPath)) - fmt.Fprintf(script, "if [[ ! -e %s ]]; then echo 'opencode shim not found after mise install' >&2; exit 1; fi\n", shellQuote(opencode.ShimPath)) - fmt.Fprintf(script, "if [[ ! -e %s ]]; then echo 'claude shim not found after mise install' >&2; exit 1; fi\n", shellQuote(claudeShimPath)) - fmt.Fprintf(script, "if [[ ! -e %s ]]; then echo 'pi shim not found after mise install' >&2; exit 1; fi\n", shellQuote(piShimPath)) - fmt.Fprintf(script, "ln -snf %s %s\n", shellQuote(nodeShimPath), shellQuote("/usr/local/bin/node")) - fmt.Fprintf(script, "ln -snf %s %s\n", shellQuote(npmShimPath), shellQuote("/usr/local/bin/npm")) - fmt.Fprintf(script, "ln -snf %s %s\n", shellQuote(opencode.ShimPath), shellQuote(opencode.GuestBinaryPath)) - fmt.Fprintf(script, "ln -snf %s %s\n", shellQuote(claudeShimPath), shellQuote("/usr/local/bin/claude")) - fmt.Fprintf(script, "ln -snf %s %s\n", shellQuote(piShimPath), shellQuote("/usr/local/bin/pi")) - script.WriteString("mkdir -p /etc/profile.d\n") - script.WriteString("cat > /etc/profile.d/mise.sh <<'EOF'\n") - fmt.Fprintf(script, "if [ -n \"${BASH_VERSION:-}\" ] && [ -x %s ]; then\n", shellQuote(defaultMiseInstallPath)) - fmt.Fprintf(script, " %s\n", defaultMiseActivateLine) - script.WriteString("fi\n") - script.WriteString("EOF\n") - script.WriteString("chmod 0644 /etc/profile.d/mise.sh\n") - appendLineIfMissing(script, "/etc/bash.bashrc", defaultMiseActivateLine) -} - -func appendGuestNetworkSetup(script *bytes.Buffer) { - script.WriteString("mkdir -p /usr/local/libexec /etc/systemd/system\n") - script.WriteString("cat > " + guestnet.GuestScriptPath + " <<'EOF'\n") - script.WriteString(guestnet.BootstrapScript()) - script.WriteString("EOF\n") - script.WriteString("chmod 0755 " + guestnet.GuestScriptPath + "\n") - script.WriteString("cat > /etc/systemd/system/" + guestnet.SystemdServiceName + " <<'EOF'\n") - script.WriteString(guestnet.SystemdServiceUnit()) - script.WriteString("EOF\n") - script.WriteString("chmod 0644 /etc/systemd/system/" + guestnet.SystemdServiceName + "\n") - script.WriteString("if command -v systemctl >/dev/null 2>&1; then systemctl daemon-reload || true; systemctl enable --now " + guestnet.SystemdServiceName + " || true; fi\n") -} - -func appendOpenCodeServiceSetup(script *bytes.Buffer) { - script.WriteString("mkdir -p /etc/systemd/system\n") - script.WriteString("cat > /etc/systemd/system/" + opencode.ServiceName + " <<'EOF'\n") - script.WriteString(opencode.ServiceUnit()) - script.WriteString("EOF\n") - script.WriteString("chmod 0644 /etc/systemd/system/" + opencode.ServiceName + "\n") - script.WriteString("if command -v systemctl >/dev/null 2>&1; then systemctl daemon-reload || true; systemctl enable --now " + opencode.ServiceName + " || true; fi\n") -} - -func appendTmuxSetup(script *bytes.Buffer) { - fmt.Fprintf(script, "TMUX_PLUGIN_DIR=%s\n", shellQuote(defaultTMUXPluginDir)) - fmt.Fprintf(script, "TMUX_RESURRECT_DIR=%s\n", shellQuote(defaultTMUXResurrectDir)) - script.WriteString("mkdir -p \"$TMUX_PLUGIN_DIR\" \"$TMUX_RESURRECT_DIR\"\n") - appendGitRepo(script, "$TMUX_PLUGIN_DIR/tpm", defaultTPMRepo) - appendGitRepo(script, "$TMUX_PLUGIN_DIR/tmux-resurrect", defaultResurrectRepo) - appendGitRepo(script, "$TMUX_PLUGIN_DIR/tmux-continuum", defaultContinuumRepo) - script.WriteString("TMUX_CONF=/root/.tmux.conf\n") - fmt.Fprintf(script, "TMUX_MANAGED_START=%s\n", shellQuote(tmuxManagedBlockStart)) - fmt.Fprintf(script, "TMUX_MANAGED_END=%s\n", shellQuote(tmuxManagedBlockEnd)) - script.WriteString("tmp_tmux_conf=$(mktemp)\n") - script.WriteString("if [[ -f \"$TMUX_CONF\" ]]; then\n") - script.WriteString(" awk -v begin=\"$TMUX_MANAGED_START\" -v end=\"$TMUX_MANAGED_END\" '$0 == begin { skip = 1; next } $0 == end { skip = 0; next } !skip { print }' \"$TMUX_CONF\" > \"$tmp_tmux_conf\"\n") - script.WriteString("else\n") - script.WriteString(" : > \"$tmp_tmux_conf\"\n") - script.WriteString("fi\n") - script.WriteString("if [[ -s \"$tmp_tmux_conf\" ]]; then\n") - script.WriteString(" printf '\\n' >> \"$tmp_tmux_conf\"\n") - script.WriteString("fi\n") - script.WriteString("cat >> \"$tmp_tmux_conf\" <<'EOF'\n") - script.WriteString(tmuxManagedBlockStart + "\n") - script.WriteString("set -g @plugin 'tmux-plugins/tpm'\n") - script.WriteString("set -g @plugin 'tmux-plugins/tmux-resurrect'\n") - script.WriteString("set -g @plugin 'tmux-plugins/tmux-continuum'\n") - script.WriteString("set -g @continuum-save-interval '15'\n") - script.WriteString("set -g @continuum-restore 'off'\n") - script.WriteString("set -g @resurrect-dir '/root/.tmux/resurrect'\n") - script.WriteString("run '~/.tmux/plugins/tpm/tpm'\n") - script.WriteString(tmuxManagedBlockEnd + "\n") - script.WriteString("EOF\n") - script.WriteString("mv \"$tmp_tmux_conf\" \"$TMUX_CONF\"\n") - script.WriteString("chmod 0644 \"$TMUX_CONF\"\n") -} - -func appendVSockPingSetup(script *bytes.Buffer) { - script.WriteString("mkdir -p /etc/modules-load.d /etc/systemd/system\n") - script.WriteString("cat > /etc/modules-load.d/banger-vsock.conf <<'EOF'\n") - script.WriteString(vsockagent.ModulesLoadConfig()) - script.WriteString("EOF\n") - script.WriteString("chmod 0644 /etc/modules-load.d/banger-vsock.conf\n") - script.WriteString("cat > /etc/systemd/system/" + vsockagent.ServiceName + " <<'EOF'\n") - script.WriteString(vsockagent.ServiceUnit()) - script.WriteString("EOF\n") - script.WriteString("chmod 0644 /etc/systemd/system/" + vsockagent.ServiceName + "\n") - script.WriteString("if command -v systemctl >/dev/null 2>&1; then systemctl daemon-reload || true; systemctl enable --now " + vsockagent.ServiceName + " || true; fi\n") -} - -func appendGitRepo(script *bytes.Buffer, dir, repo string) { - fmt.Fprintf(script, "if [[ -d \"%s/.git\" ]]; then\n", dir) - fmt.Fprintf(script, " git -C \"%s\" fetch --depth 1 origin\n", dir) - fmt.Fprintf(script, " git -C \"%s\" reset --hard FETCH_HEAD\n", dir) - script.WriteString("else\n") - fmt.Fprintf(script, " rm -rf \"%s\"\n", dir) - fmt.Fprintf(script, " git clone --depth 1 %s \"%s\"\n", shellQuote(repo), dir) - script.WriteString("fi\n") -} - -func appendGuestCleanup(script *bytes.Buffer) { - script.WriteString("rm -f /root/get-docker /root/get-docker.sh /tmp/get-docker /tmp/get-docker.sh\n") -} - -func appendLineIfMissing(script *bytes.Buffer, path, line string) { - fmt.Fprintf(script, "touch %s\n", shellQuote(path)) - fmt.Fprintf(script, "if ! grep -Fqx %s %s; then\n", shellQuote(line), shellQuote(path)) - fmt.Fprintf(script, " printf '\\n%%s\\n' %s >> %s\n", shellQuote(line), shellQuote(path)) - script.WriteString("fi\n") -} - -func shellArray(values []string) string { - quoted := make([]string, 0, len(values)) - for _, value := range values { - quoted = append(quoted, shellQuote(value)) - } - return "(" + strings.Join(quoted, " ") + ")" -} - -func shellQuote(value string) string { - return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'" -} - -func writeBuildLog(w io.Writer, message string) error { - if w == nil { - return nil - } - _, err := fmt.Fprintf(w, "[image.build] %s\n", message) - return err -} - -func packagesHash(lines []string) string { - return imagepreset.Hash(lines) -} diff --git a/internal/daemon/imagebuild_test.go b/internal/daemon/imagebuild_test.go index af7662d..6ad8731 100644 --- a/internal/daemon/imagebuild_test.go +++ b/internal/daemon/imagebuild_test.go @@ -3,12 +3,14 @@ package daemon import ( "strings" "testing" + + "banger/internal/daemon/imagemgr" ) func TestBuildProvisionScriptInstallsDefaultTools(t *testing.T) { t.Parallel() - script := buildProvisionScript("devbox", "1.1.1.1", "ssh-ed25519 AAAATESTKEY banger", []string{"git", "curl"}, false) + script := imagemgr.BuildProvisionScript("devbox", "1.1.1.1", "ssh-ed25519 AAAATESTKEY banger", []string{"git", "curl"}, false) for _, snippet := range []string{ "mkdir -p /root/.ssh", "cat > /root/.ssh/authorized_keys <<'EOF'", @@ -59,7 +61,7 @@ func TestBuildProvisionScriptInstallsDefaultTools(t *testing.T) { "rm -f /root/get-docker /root/get-docker.sh /tmp/get-docker /tmp/get-docker.sh", } { if !strings.Contains(script, snippet) { - t.Fatalf("buildProvisionScript missing snippet %q\nscript:\n%s", snippet, script) + t.Fatalf("BuildProvisionScript missing snippet %q\nscript:\n%s", snippet, script) } } } diff --git a/internal/daemon/imagemgr/build.go b/internal/daemon/imagemgr/build.go new file mode 100644 index 0000000..3bffcf9 --- /dev/null +++ b/internal/daemon/imagemgr/build.go @@ -0,0 +1,248 @@ +package imagemgr + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "strings" + + "banger/internal/guestnet" + "banger/internal/model" + "banger/internal/opencode" + "banger/internal/system" + "banger/internal/vsockagent" +) + +const ( + defaultMiseVersion = "v2025.12.0" + defaultMiseInstallPath = "/usr/local/bin/mise" + defaultMiseActivateLine = `eval "$(/usr/local/bin/mise activate bash)"` + defaultNodeTool = "node@22" + defaultOpenCodeTool = "github:anomalyco/opencode" + defaultClaudeCodeTool = "npm:@anthropic-ai/claude-code" + defaultPiTool = "npm:@mariozechner/pi-coding-agent" + defaultTPMRepo = "https://github.com/tmux-plugins/tpm" + defaultResurrectRepo = "https://github.com/tmux-plugins/tmux-resurrect" + defaultContinuumRepo = "https://github.com/tmux-plugins/tmux-continuum" + defaultTMUXPluginDir = "/root/.tmux/plugins" + defaultTMUXResurrectDir = "/root/.tmux/resurrect" + tmuxManagedBlockStart = "# >>> banger tmux plugins >>>" + tmuxManagedBlockEnd = "# <<< banger tmux plugins <<<" +) + +// ResizeRootfs grows a rootfs ext4 image to sizeSpec bytes. sizeSpec must +// parse via model.ParseSize and must be >= the base image size. +func ResizeRootfs(baseRootfs, rootfsPath, sizeSpec string) error { + sizeBytes, err := model.ParseSize(sizeSpec) + if err != nil { + return err + } + info, err := os.Stat(baseRootfs) + if err != nil { + return err + } + if sizeBytes < info.Size() { + return fmt.Errorf("size must be >= base image size") + } + return system.ResizeExt4Image(context.Background(), system.NewRunner(), rootfsPath, sizeBytes) +} + +// WriteBuildLog emits a prefixed status line to w. Safe on a nil writer. +func WriteBuildLog(w io.Writer, message string) error { + if w == nil { + return nil + } + _, err := fmt.Fprintf(w, "[image.build] %s\n", message) + return err +} + +// BuildProvisionScript returns the bash script that configures a freshly +// booted build VM: host/dns files, authorized key, apt packages, mise + +// language shims, guest network unit, opencode service, tmux plugins, +// vsock agent, optional docker, and cleanup. +func BuildProvisionScript(vmName, dnsServer, authorizedKey string, packages []string, installDocker bool) string { + var script bytes.Buffer + script.WriteString("set -euo pipefail\n") + fmt.Fprintf(&script, "printf 'nameserver %%s\\n' %s > /etc/resolv.conf\n", shellQuote(dnsServer)) + fmt.Fprintf(&script, "printf '%%s\\n' %s > /etc/hostname\n", shellQuote(vmName)) + fmt.Fprintf(&script, "printf '127.0.0.1 localhost\\n127.0.1.1 %%s\\n' %s > /etc/hosts\n", shellQuote(vmName)) + script.WriteString("touch /etc/fstab\n") + script.WriteString("sed -i '\\|^/dev/vdb[[:space:]]\\+/home[[:space:]]|d; \\|^/dev/vdc[[:space:]]\\+/var[[:space:]]|d' /etc/fstab\n") + script.WriteString("if ! grep -q '^tmpfs /run ' /etc/fstab; then echo 'tmpfs /run tmpfs defaults,nodev,nosuid,mode=0755 0 0' >> /etc/fstab; fi\n") + script.WriteString("if ! grep -q '^tmpfs /tmp ' /etc/fstab; then echo 'tmpfs /tmp tmpfs defaults,nodev,nosuid,mode=1777 0 0' >> /etc/fstab; fi\n") + appendAuthorizedKeySetup(&script, authorizedKey) + script.WriteString("apt-get update\n") + script.WriteString("DEBIAN_FRONTEND=noninteractive apt-get -y upgrade\n") + fmt.Fprintf(&script, "PACKAGES=%s\n", shellArray(packages)) + script.WriteString("DEBIAN_FRONTEND=noninteractive apt-get -y install \"${PACKAGES[@]}\"\n") + appendGuestNetworkSetup(&script) + appendMiseSetup(&script) + appendOpenCodeServiceSetup(&script) + appendTmuxSetup(&script) + appendVSockPingSetup(&script) + if installDocker { + script.WriteString("DEBIAN_FRONTEND=noninteractive apt-get -y remove containerd || true\n") + script.WriteString("if ! DEBIAN_FRONTEND=noninteractive apt-get -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; then\n") + script.WriteString(" DEBIAN_FRONTEND=noninteractive apt-get -y install docker.io\n") + script.WriteString("fi\n") + script.WriteString("if command -v systemctl >/dev/null 2>&1; then systemctl enable --now docker || true; fi\n") + } + appendGuestCleanup(&script) + script.WriteString("git config --system init.defaultBranch main\n") + return script.String() +} + +// BuildModulesCommand returns the guest shell command that receives a tar +// stream on stdin, extracts it into /lib/modules/, runs depmod, +// and writes sysctl/modules-load config for docker networking. +func BuildModulesCommand(modulesBase string) string { + return fmt.Sprintf("bash -se <<'EOF'\nset -euo pipefail\nmkdir -p /lib/modules\ntar -C /lib/modules -xf -\ndepmod -a %s\nmkdir -p /etc/modules-load.d\nprintf 'nf_tables\\nnft_chain_nat\\nveth\\nbr_netfilter\\noverlay\\n' > /etc/modules-load.d/docker-netfilter.conf\nmkdir -p /etc/sysctl.d\ncat > /etc/sysctl.d/99-docker.conf <<'SYSCTL'\nnet.bridge.bridge-nf-call-iptables = 1\nnet.bridge.bridge-nf-call-ip6tables = 1\nnet.ipv4.ip_forward = 1\nSYSCTL\nsysctl --system >/dev/null 2>&1 || true\nEOF", shellQuote(modulesBase)) +} + +func appendAuthorizedKeySetup(script *bytes.Buffer, authorizedKey string) { + script.WriteString("mkdir -p /root/.ssh\n") + script.WriteString("chmod 700 /root/.ssh\n") + script.WriteString("cat > /root/.ssh/authorized_keys <<'EOF'\n") + script.WriteString(strings.TrimSpace(authorizedKey)) + script.WriteString("\nEOF\n") + script.WriteString("chmod 600 /root/.ssh/authorized_keys\n") +} + +func appendMiseSetup(script *bytes.Buffer) { + const ( + nodeShimPath = "/root/.local/share/mise/shims/node" + npmShimPath = "/root/.local/share/mise/shims/npm" + claudeShimPath = "/root/.local/share/mise/shims/claude" + piShimPath = "/root/.local/share/mise/shims/pi" + ) + + fmt.Fprintf(script, "curl -fsSL https://mise.run | MISE_INSTALL_PATH=%s MISE_VERSION=%s sh\n", shellQuote(defaultMiseInstallPath), shellQuote(defaultMiseVersion)) + fmt.Fprintf(script, "%s use -g %s\n", shellQuote(defaultMiseInstallPath), shellQuote(defaultNodeTool)) + fmt.Fprintf(script, "%s use -g %s\n", shellQuote(defaultMiseInstallPath), shellQuote(defaultOpenCodeTool)) + fmt.Fprintf(script, "%s use -g %s\n", shellQuote(defaultMiseInstallPath), shellQuote(defaultClaudeCodeTool)) + fmt.Fprintf(script, "%s use -g %s\n", shellQuote(defaultMiseInstallPath), shellQuote(defaultPiTool)) + fmt.Fprintf(script, "%s reshim\n", shellQuote(defaultMiseInstallPath)) + fmt.Fprintf(script, "if [[ ! -e %s ]]; then echo 'node shim not found after mise install' >&2; exit 1; fi\n", shellQuote(nodeShimPath)) + fmt.Fprintf(script, "if [[ ! -e %s ]]; then echo 'npm shim not found after mise install' >&2; exit 1; fi\n", shellQuote(npmShimPath)) + fmt.Fprintf(script, "if [[ ! -e %s ]]; then echo 'opencode shim not found after mise install' >&2; exit 1; fi\n", shellQuote(opencode.ShimPath)) + fmt.Fprintf(script, "if [[ ! -e %s ]]; then echo 'claude shim not found after mise install' >&2; exit 1; fi\n", shellQuote(claudeShimPath)) + fmt.Fprintf(script, "if [[ ! -e %s ]]; then echo 'pi shim not found after mise install' >&2; exit 1; fi\n", shellQuote(piShimPath)) + fmt.Fprintf(script, "ln -snf %s %s\n", shellQuote(nodeShimPath), shellQuote("/usr/local/bin/node")) + fmt.Fprintf(script, "ln -snf %s %s\n", shellQuote(npmShimPath), shellQuote("/usr/local/bin/npm")) + fmt.Fprintf(script, "ln -snf %s %s\n", shellQuote(opencode.ShimPath), shellQuote(opencode.GuestBinaryPath)) + fmt.Fprintf(script, "ln -snf %s %s\n", shellQuote(claudeShimPath), shellQuote("/usr/local/bin/claude")) + fmt.Fprintf(script, "ln -snf %s %s\n", shellQuote(piShimPath), shellQuote("/usr/local/bin/pi")) + script.WriteString("mkdir -p /etc/profile.d\n") + script.WriteString("cat > /etc/profile.d/mise.sh <<'EOF'\n") + fmt.Fprintf(script, "if [ -n \"${BASH_VERSION:-}\" ] && [ -x %s ]; then\n", shellQuote(defaultMiseInstallPath)) + fmt.Fprintf(script, " %s\n", defaultMiseActivateLine) + script.WriteString("fi\n") + script.WriteString("EOF\n") + script.WriteString("chmod 0644 /etc/profile.d/mise.sh\n") + appendLineIfMissing(script, "/etc/bash.bashrc", defaultMiseActivateLine) +} + +func appendGuestNetworkSetup(script *bytes.Buffer) { + script.WriteString("mkdir -p /usr/local/libexec /etc/systemd/system\n") + script.WriteString("cat > " + guestnet.GuestScriptPath + " <<'EOF'\n") + script.WriteString(guestnet.BootstrapScript()) + script.WriteString("EOF\n") + script.WriteString("chmod 0755 " + guestnet.GuestScriptPath + "\n") + script.WriteString("cat > /etc/systemd/system/" + guestnet.SystemdServiceName + " <<'EOF'\n") + script.WriteString(guestnet.SystemdServiceUnit()) + script.WriteString("EOF\n") + script.WriteString("chmod 0644 /etc/systemd/system/" + guestnet.SystemdServiceName + "\n") + script.WriteString("if command -v systemctl >/dev/null 2>&1; then systemctl daemon-reload || true; systemctl enable --now " + guestnet.SystemdServiceName + " || true; fi\n") +} + +func appendOpenCodeServiceSetup(script *bytes.Buffer) { + script.WriteString("mkdir -p /etc/systemd/system\n") + script.WriteString("cat > /etc/systemd/system/" + opencode.ServiceName + " <<'EOF'\n") + script.WriteString(opencode.ServiceUnit()) + script.WriteString("EOF\n") + script.WriteString("chmod 0644 /etc/systemd/system/" + opencode.ServiceName + "\n") + script.WriteString("if command -v systemctl >/dev/null 2>&1; then systemctl daemon-reload || true; systemctl enable --now " + opencode.ServiceName + " || true; fi\n") +} + +func appendTmuxSetup(script *bytes.Buffer) { + fmt.Fprintf(script, "TMUX_PLUGIN_DIR=%s\n", shellQuote(defaultTMUXPluginDir)) + fmt.Fprintf(script, "TMUX_RESURRECT_DIR=%s\n", shellQuote(defaultTMUXResurrectDir)) + script.WriteString("mkdir -p \"$TMUX_PLUGIN_DIR\" \"$TMUX_RESURRECT_DIR\"\n") + appendGitRepo(script, "$TMUX_PLUGIN_DIR/tpm", defaultTPMRepo) + appendGitRepo(script, "$TMUX_PLUGIN_DIR/tmux-resurrect", defaultResurrectRepo) + appendGitRepo(script, "$TMUX_PLUGIN_DIR/tmux-continuum", defaultContinuumRepo) + script.WriteString("TMUX_CONF=/root/.tmux.conf\n") + fmt.Fprintf(script, "TMUX_MANAGED_START=%s\n", shellQuote(tmuxManagedBlockStart)) + fmt.Fprintf(script, "TMUX_MANAGED_END=%s\n", shellQuote(tmuxManagedBlockEnd)) + script.WriteString("tmp_tmux_conf=$(mktemp)\n") + script.WriteString("if [[ -f \"$TMUX_CONF\" ]]; then\n") + script.WriteString(" awk -v begin=\"$TMUX_MANAGED_START\" -v end=\"$TMUX_MANAGED_END\" '$0 == begin { skip = 1; next } $0 == end { skip = 0; next } !skip { print }' \"$TMUX_CONF\" > \"$tmp_tmux_conf\"\n") + script.WriteString("else\n") + script.WriteString(" : > \"$tmp_tmux_conf\"\n") + script.WriteString("fi\n") + script.WriteString("if [[ -s \"$tmp_tmux_conf\" ]]; then\n") + script.WriteString(" printf '\\n' >> \"$tmp_tmux_conf\"\n") + script.WriteString("fi\n") + script.WriteString("cat >> \"$tmp_tmux_conf\" <<'EOF'\n") + script.WriteString(tmuxManagedBlockStart + "\n") + script.WriteString("set -g @plugin 'tmux-plugins/tpm'\n") + script.WriteString("set -g @plugin 'tmux-plugins/tmux-resurrect'\n") + script.WriteString("set -g @plugin 'tmux-plugins/tmux-continuum'\n") + script.WriteString("set -g @continuum-save-interval '15'\n") + script.WriteString("set -g @continuum-restore 'off'\n") + script.WriteString("set -g @resurrect-dir '/root/.tmux/resurrect'\n") + script.WriteString("run '~/.tmux/plugins/tpm/tpm'\n") + script.WriteString(tmuxManagedBlockEnd + "\n") + script.WriteString("EOF\n") + script.WriteString("mv \"$tmp_tmux_conf\" \"$TMUX_CONF\"\n") + script.WriteString("chmod 0644 \"$TMUX_CONF\"\n") +} + +func appendVSockPingSetup(script *bytes.Buffer) { + script.WriteString("mkdir -p /etc/modules-load.d /etc/systemd/system\n") + script.WriteString("cat > /etc/modules-load.d/banger-vsock.conf <<'EOF'\n") + script.WriteString(vsockagent.ModulesLoadConfig()) + script.WriteString("EOF\n") + script.WriteString("chmod 0644 /etc/modules-load.d/banger-vsock.conf\n") + script.WriteString("cat > /etc/systemd/system/" + vsockagent.ServiceName + " <<'EOF'\n") + script.WriteString(vsockagent.ServiceUnit()) + script.WriteString("EOF\n") + script.WriteString("chmod 0644 /etc/systemd/system/" + vsockagent.ServiceName + "\n") + script.WriteString("if command -v systemctl >/dev/null 2>&1; then systemctl daemon-reload || true; systemctl enable --now " + vsockagent.ServiceName + " || true; fi\n") +} + +func appendGitRepo(script *bytes.Buffer, dir, repo string) { + fmt.Fprintf(script, "if [[ -d \"%s/.git\" ]]; then\n", dir) + fmt.Fprintf(script, " git -C \"%s\" fetch --depth 1 origin\n", dir) + fmt.Fprintf(script, " git -C \"%s\" reset --hard FETCH_HEAD\n", dir) + script.WriteString("else\n") + fmt.Fprintf(script, " rm -rf \"%s\"\n", dir) + fmt.Fprintf(script, " git clone --depth 1 %s \"%s\"\n", shellQuote(repo), dir) + script.WriteString("fi\n") +} + +func appendGuestCleanup(script *bytes.Buffer) { + script.WriteString("rm -f /root/get-docker /root/get-docker.sh /tmp/get-docker /tmp/get-docker.sh\n") +} + +func appendLineIfMissing(script *bytes.Buffer, path, line string) { + fmt.Fprintf(script, "touch %s\n", shellQuote(path)) + fmt.Fprintf(script, "if ! grep -Fqx %s %s; then\n", shellQuote(line), shellQuote(path)) + fmt.Fprintf(script, " printf '\\n%%s\\n' %s >> %s\n", shellQuote(line), shellQuote(path)) + script.WriteString("fi\n") +} + +func shellArray(values []string) string { + quoted := make([]string, 0, len(values)) + for _, value := range values { + quoted = append(quoted, shellQuote(value)) + } + return "(" + strings.Join(quoted, " ") + ")" +} + +func shellQuote(value string) string { + return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'" +} + diff --git a/internal/daemon/imagemgr/paths.go b/internal/daemon/imagemgr/paths.go new file mode 100644 index 0000000..12996d6 --- /dev/null +++ b/internal/daemon/imagemgr/paths.go @@ -0,0 +1,108 @@ +// Package imagemgr contains the pure helpers of the banger image subsystem: +// path validators, artifact staging, managed-image metadata, and the guest +// provisioning script generator used by image build. +// +// The orchestrator methods (BuildImage, RegisterImage, PromoteImage, +// DeleteImage) still live in the daemon package and compose these helpers. +package imagemgr + +import ( + "context" + "os" + "path/filepath" + "strings" + + "banger/internal/imagepreset" + "banger/internal/system" +) + +// ValidateRegisterPaths checks that rootfs + kernel exist and that optional +// artifacts, when provided, also exist. +func ValidateRegisterPaths(rootfsPath, workSeedPath, kernelPath, initrdPath, modulesDir string) error { + checks := system.NewPreflight() + checks.RequireFile(rootfsPath, "rootfs image", `pass --rootfs `) + checks.RequireFile(kernelPath, "kernel image", `pass --kernel `) + if workSeedPath != "" { + checks.RequireFile(workSeedPath, "work-seed image", `pass --work-seed or rebuild the image with a work seed`) + } + if initrdPath != "" { + checks.RequireFile(initrdPath, "initrd image", `pass --initrd `) + } + if modulesDir != "" { + checks.RequireDir(modulesDir, "kernel modules dir", `pass --modules `) + } + return checks.Err("image register failed") +} + +// ValidatePromotePaths checks that an existing registered image's artifacts +// are still present before promoting it to daemon-owned storage. +func ValidatePromotePaths(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`) + if initrdPath != "" { + checks.RequireFile(initrdPath, "initrd image", `re-register the image with a valid initrd`) + } + if modulesDir != "" { + checks.RequireDir(modulesDir, "kernel modules dir", `re-register the image with a valid modules dir`) + } + return checks.Err("image promote failed") +} + +// StageBootArtifacts copies kernel/initrd/modules into artifactDir and +// returns the staged paths. initrd and modules are optional; an empty source +// returns an empty staged path. +func StageBootArtifacts(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 +} + +// StageOptionalArtifactPath returns the destination path for an optional +// artifact in artifactDir, or "" when stagedPath is empty (artifact absent). +func StageOptionalArtifactPath(artifactDir, stagedPath, name string) string { + if strings.TrimSpace(stagedPath) == "" { + return "" + } + return filepath.Join(artifactDir, name) +} + +// BuildMetadataPackages returns the canonical package set recorded for a +// managed image build. The #feature:docker sentinel is appended when +// docker is requested. +func BuildMetadataPackages(docker bool) []string { + packages := imagepreset.DebianBasePackages() + if docker { + packages = append(packages, "#feature:docker") + } + return packages +} + +// WritePackagesMetadata writes the hash of packages next to rootfsPath so +// future builds can detect drift. Empty packages or rootfsPath is a no-op. +func WritePackagesMetadata(rootfsPath string, packages []string) error { + if rootfsPath == "" || len(packages) == 0 { + return nil + } + metadataPath := rootfsPath + ".packages.sha256" + return os.WriteFile(metadataPath, []byte(imagepreset.Hash(packages)+"\n"), 0o644) +} diff --git a/internal/daemon/images.go b/internal/daemon/images.go index 1768b05..d724b76 100644 --- a/internal/daemon/images.go +++ b/internal/daemon/images.go @@ -10,6 +10,7 @@ import ( "strings" "banger/internal/api" + "banger/internal/daemon/imagemgr" "banger/internal/imagepreset" "banger/internal/model" "banger/internal/system" @@ -80,12 +81,12 @@ func (d *Daemon) BuildImage(ctx context.Context, params api.ImageBuildParams) (i 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) + kernelPath, initrdPath, modulesDir, err := imagemgr.StageBootArtifacts(ctx, d.runner, stageDir, kernelSource, initrdSource, modulesSource) if err != nil { return model.Image{}, err } packages := imagepreset.DebianBasePackages() - metadataPackages := imageBuildMetadataPackages(params.Docker) + metadataPackages := imagemgr.BuildMetadataPackages(params.Docker) spec := imageBuildSpec{ ID: id, Name: name, @@ -117,7 +118,7 @@ func (d *Daemon) BuildImage(ctx context.Context, params api.ImageBuildParams) (i return model.Image{}, err } imageBuildStage(ctx, "write_metadata", "writing image metadata") - if err := writePackagesMetadata(rootfsPath, metadataPackages); err != nil { + if err := imagemgr.WritePackagesMetadata(rootfsPath, metadataPackages); err != nil { _ = logFile.Sync() return model.Image{}, err } @@ -134,8 +135,8 @@ func (d *Daemon) BuildImage(ctx context.Context, params api.ImageBuildParams) (i 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"), + InitrdPath: imagemgr.StageOptionalArtifactPath(artifactDir, initrdPath, "initrd.img"), + ModulesDir: imagemgr.StageOptionalArtifactPath(artifactDir, modulesDir, "modules"), BuildSize: params.Size, SeededSSHPublicKeyFingerprint: seededSSHPublicKeyFingerprint, Docker: params.Docker, @@ -184,7 +185,7 @@ func (d *Daemon) RegisterImage(ctx context.Context, params api.ImageRegisterPara initrdPath := strings.TrimSpace(params.InitrdPath) modulesDir := strings.TrimSpace(params.ModulesDir) - if err := validateImageRegisterPaths(rootfsPath, workSeedPath, kernelPath, initrdPath, modulesDir); err != nil { + if err := imagemgr.ValidateRegisterPaths(rootfsPath, workSeedPath, kernelPath, initrdPath, modulesDir); err != nil { return model.Image{}, err } @@ -251,7 +252,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); err != nil { + if err := imagemgr.ValidatePromotePaths(image.RootfsPath, image.KernelPath, image.InitrdPath, image.ModulesDir); err != nil { return model.Image{}, err } if strings.TrimSpace(d.layout.ImagesDir) == "" { @@ -309,7 +310,7 @@ 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) + _, initrdPath, modulesDir, err := imagemgr.StageBootArtifacts(ctx, d.runner, stageDir, image.KernelPath, image.InitrdPath, image.ModulesDir) if err != nil { return model.Image{}, err } @@ -327,8 +328,8 @@ func (d *Daemon) PromoteImage(ctx context.Context, idOrName string) (image model 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.InitrdPath = imagemgr.StageOptionalArtifactPath(artifactDir, initrdPath, "initrd.img") + image.ModulesDir = imagemgr.StageOptionalArtifactPath(artifactDir, modulesDir, "modules") image.UpdatedAt = model.Now() if err := d.store.UpsertImage(ctx, image); err != nil { _ = os.RemoveAll(artifactDir) @@ -337,43 +338,6 @@ func (d *Daemon) PromoteImage(ctx context.Context, idOrName string) (image model return image, nil } -func validateImageRegisterPaths(rootfsPath, workSeedPath, kernelPath, initrdPath, modulesDir string) error { - checks := system.NewPreflight() - checks.RequireFile(rootfsPath, "rootfs image", `pass --rootfs `) - checks.RequireFile(kernelPath, "kernel image", `pass --kernel `) - if workSeedPath != "" { - checks.RequireFile(workSeedPath, "work-seed image", `pass --work-seed or rebuild the image with a work seed`) - } - if initrdPath != "" { - checks.RequireFile(initrdPath, "initrd image", `pass --initrd `) - } - if modulesDir != "" { - checks.RequireDir(modulesDir, "kernel modules dir", `pass --modules `) - } - return checks.Err("image register failed") -} - -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`) - if initrdPath != "" { - checks.RequireFile(initrdPath, "initrd image", `re-register the image with a valid initrd`) - } - if modulesDir != "" { - checks.RequireDir(modulesDir, "kernel modules dir", `re-register the image with a valid modules dir`) - } - return checks.Err("image promote failed") -} - -func writePackagesMetadata(rootfsPath string, packages []string) error { - if rootfsPath == "" || len(packages) == 0 { - return nil - } - metadataPath := rootfsPath + ".packages.sha256" - return os.WriteFile(metadataPath, []byte(packagesHash(packages)+"\n"), 0o644) -} - func (d *Daemon) DeleteImage(ctx context.Context, idOrName string) (model.Image, error) { d.imageOpsMu.Lock() defer d.imageOpsMu.Unlock() @@ -400,46 +364,6 @@ 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) != "" {