Provisioning was still installing `claude` and `pi` through a separate npm-global prefix even after the guest images had switched to `mise` for Node and opencode. That left two competing install paths and made the runtime layout harder to reason about. Switch the Debian and Void image setup flows to install `claude` and `pi` as `mise` npm tools, assert their shims exist after `mise reshim`, and symlink `node`, `npm`, `opencode`, `claude`, and `pi` directly from the mise shim directory into `/usr/local/bin`. Update the imagebuild test expectations and bump the Void rootfs default size to 4G so the larger default toolset still fits reliably.
453 lines
19 KiB
Go
453 lines
19 KiB
Go
package daemon
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"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 <<<"
|
|
)
|
|
|
|
type imageBuildSpec struct {
|
|
ID string
|
|
Name string
|
|
SourceRootfs string
|
|
RootfsPath string
|
|
BuildLog io.Writer
|
|
KernelPath string
|
|
InitrdPath string
|
|
ModulesDir string
|
|
Packages []string
|
|
InstallDocker bool
|
|
Size string
|
|
}
|
|
|
|
type imageBuildVM struct {
|
|
Name string
|
|
GuestIP string
|
|
TapDevice string
|
|
APISock string
|
|
PID int
|
|
}
|
|
|
|
func (d *Daemon) runImageBuild(ctx context.Context, spec imageBuildSpec) error {
|
|
if d.imageBuild != nil {
|
|
return d.imageBuild(ctx, spec)
|
|
}
|
|
return d.runImageBuildNative(ctx, spec)
|
|
}
|
|
|
|
func (d *Daemon) runImageBuildNative(ctx context.Context, spec imageBuildSpec) (err error) {
|
|
if err := system.CopyFilePreferClone(spec.SourceRootfs, spec.RootfsPath); err != nil {
|
|
return err
|
|
}
|
|
if spec.Size != "" {
|
|
if err := resizeRootfs(spec.SourceRootfs, spec.RootfsPath, spec.Size); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
vm, cleanup, err := d.startImageBuildVM(ctx, spec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
cleanupErr := cleanup(context.Background())
|
|
if cleanupErr != nil {
|
|
err = errors.Join(err, cleanupErr)
|
|
}
|
|
}()
|
|
|
|
sshAddress := vm.GuestIP + ":22"
|
|
if _, err := fmt.Fprintf(spec.BuildLog, "[image.build] waiting for ssh on %s\n", sshAddress); err != nil {
|
|
return err
|
|
}
|
|
waitCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
|
|
defer cancel()
|
|
if err := guest.WaitForSSH(waitCtx, sshAddress, d.config.SSHKeyPath, time.Second); err != nil {
|
|
return err
|
|
}
|
|
|
|
client, err := guest.Dial(ctx, sshAddress, d.config.SSHKeyPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer client.Close()
|
|
authorizedKey, err := guest.AuthorizedPublicKey(d.config.SSHKeyPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
vsockAgentPath, err := d.vsockAgentBinary()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
helperBytes, err := os.ReadFile(vsockAgentPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := 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 {
|
|
return err
|
|
}
|
|
if err := client.RunScript(ctx, 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 {
|
|
return err
|
|
}
|
|
if err := client.StreamTar(ctx, spec.ModulesDir, buildModulesCommand(filepath.Base(spec.ModulesDir)), spec.BuildLog); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := writeBuildLog(spec.BuildLog, "shutting down guest"); err != nil {
|
|
return err
|
|
}
|
|
if err := client.RunScript(ctx, "set -e\nsync\n", spec.BuildLog); err != nil {
|
|
return err
|
|
}
|
|
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
|
|
}
|
|
if err := d.ensureSocketDir(); err != nil {
|
|
return imageBuildVM{}, nil, err
|
|
}
|
|
fcPath, err := d.firecrackerBinary()
|
|
if err != nil {
|
|
return imageBuildVM{}, nil, err
|
|
}
|
|
|
|
shortID := system.ShortID(spec.ID)
|
|
guestIP, err := d.store.NextGuestIP(ctx, bridgePrefix(d.config.BridgeIP))
|
|
if err != nil {
|
|
return imageBuildVM{}, nil, err
|
|
}
|
|
vm := imageBuildVM{
|
|
Name: "image-build-" + shortID,
|
|
GuestIP: guestIP,
|
|
TapDevice: "tap-img-" + shortID,
|
|
APISock: filepath.Join(d.layout.RuntimeDir, "img-"+shortID+".sock"),
|
|
}
|
|
if err := os.RemoveAll(vm.APISock); err != nil && !os.IsNotExist(err) {
|
|
return imageBuildVM{}, nil, err
|
|
}
|
|
if err := d.createTap(ctx, vm.TapDevice); err != nil {
|
|
return imageBuildVM{}, nil, err
|
|
}
|
|
if err := hostnat.Ensure(ctx, d.runner, vm.GuestIP, vm.TapDevice, true); err != nil {
|
|
_, _ = d.runner.RunSudo(ctx, "ip", "link", "del", vm.TapDevice)
|
|
return imageBuildVM{}, nil, err
|
|
}
|
|
|
|
firecrackerCtx := context.Background()
|
|
machine, err := firecracker.NewMachine(firecrackerCtx, firecracker.MachineConfig{
|
|
BinaryPath: fcPath,
|
|
VMID: spec.ID,
|
|
SocketPath: vm.APISock,
|
|
LogPath: spec.RootfsPath + ".firecracker.log",
|
|
MetricsPath: filepath.Join(filepath.Dir(spec.RootfsPath), "metrics.json"),
|
|
KernelImagePath: spec.KernelPath,
|
|
InitrdPath: spec.InitrdPath,
|
|
KernelArgs: system.BuildBootArgsWithKernelIP(vm.Name, vm.GuestIP, d.config.BridgeIP, d.config.DefaultDNS),
|
|
Drives: []firecracker.DriveConfig{{
|
|
ID: "rootfs",
|
|
Path: spec.RootfsPath,
|
|
ReadOnly: false,
|
|
IsRoot: true,
|
|
}},
|
|
TapDevice: vm.TapDevice,
|
|
VCPUCount: model.DefaultVCPUCount,
|
|
MemoryMiB: model.DefaultMemoryMiB,
|
|
Logger: d.logger,
|
|
})
|
|
if err != nil {
|
|
_ = hostnat.Ensure(ctx, d.runner, vm.GuestIP, vm.TapDevice, false)
|
|
_, _ = d.runner.RunSudo(ctx, "ip", "link", "del", vm.TapDevice)
|
|
return imageBuildVM{}, nil, err
|
|
}
|
|
if err := machine.Start(firecrackerCtx); err != nil {
|
|
_ = hostnat.Ensure(ctx, d.runner, vm.GuestIP, vm.TapDevice, false)
|
|
_, _ = d.runner.RunSudo(ctx, "ip", "link", "del", vm.TapDevice)
|
|
return imageBuildVM{}, nil, err
|
|
}
|
|
vm.PID = d.resolveFirecrackerPID(firecrackerCtx, machine, vm.APISock)
|
|
if err := d.ensureSocketAccess(ctx, vm.APISock, "firecracker api socket"); err != nil {
|
|
_ = d.killVMProcess(context.Background(), vm.PID)
|
|
_ = hostnat.Ensure(ctx, d.runner, vm.GuestIP, vm.TapDevice, false)
|
|
_, _ = d.runner.RunSudo(ctx, "ip", "link", "del", vm.TapDevice)
|
|
return imageBuildVM{}, nil, err
|
|
}
|
|
|
|
cleanup := func(cleanupCtx context.Context) error {
|
|
if vm.PID > 0 && system.ProcessRunning(vm.PID, vm.APISock) {
|
|
_ = d.killVMProcess(cleanupCtx, vm.PID)
|
|
_ = d.waitForExit(cleanupCtx, vm.PID, vm.APISock, 10*time.Second)
|
|
}
|
|
_ = hostnat.Ensure(cleanupCtx, d.runner, vm.GuestIP, vm.TapDevice, false)
|
|
if vm.TapDevice != "" {
|
|
_, _ = d.runner.RunSudo(cleanupCtx, "ip", "link", "del", vm.TapDevice)
|
|
}
|
|
if vm.APISock != "" {
|
|
_ = os.Remove(vm.APISock)
|
|
}
|
|
return nil
|
|
}
|
|
return vm, cleanup, nil
|
|
}
|
|
|
|
func (d *Daemon) shutdownImageBuildVM(ctx context.Context, vm imageBuildVM) error {
|
|
buildVM := model.VMRecord{Runtime: model.VMRuntime{APISockPath: vm.APISock}}
|
|
if err := d.sendCtrlAltDel(ctx, buildVM); err != nil {
|
|
return err
|
|
}
|
|
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)
|
|
}
|