Add tmux resurrect defaults to rebuilt images

New VMs should come up with tmux session persistence ready instead of requiring per-VM plugin setup, and rebuilt images should stop carrying stale Docker installer scraps.

Configure both image build paths to install TPM, tmux-resurrect, and tmux-continuum for root, manage a marked /root/.tmux.conf block with autosave enabled and restore left manual, and remove legacy get-docker helper files during provisioning.

Update the README and repo guidance to document the rebuilt-image behavior. Verified with bash -n customize.sh, GOCACHE=/tmp/banger-gocache go test ./internal/daemon -run TestBuildProvisionScriptInstallsDefaultTools, and GOCACHE=/tmp/banger-gocache make build.
This commit is contained in:
Thales Maciel 2026-03-18 17:44:12 -03:00
parent 3a92362829
commit 38d7eac430
No known key found for this signature in database
GPG key ID: 33112E6833C34679
5 changed files with 133 additions and 4 deletions

View file

@ -28,7 +28,7 @@
## Testing Guidelines ## Testing Guidelines
- Primary automated coverage is `go test ./...`. - Primary automated coverage is `go test ./...`.
- Manual verification for VM lifecycle changes: `./banger vm create`, confirm SSH access, then stop/delete the VM. - Manual verification for VM lifecycle changes: `./banger vm create`, confirm SSH access, then stop/delete the VM.
- Rebuilt images now include `mise` plus `opencode` by default; if you change guest provisioning, document whether users need to rebuild `./runtime/rootfs-docker.ext4` or another base image to pick it up. - Rebuilt images now include `mise`, `opencode`, and `tmux-resurrect`/`tmux-continuum` defaults for `root`; if you change guest provisioning, document whether users need to rebuild `./runtime/rootfs-docker.ext4` or another base image to pick it up.
- If you add a new operational workflow, document how to exercise it in `README.md`. - If you add a new operational workflow, document how to exercise it in `README.md`.
- For NAT changes, verify both guest outbound access and host rule cleanup, for example with `./verify.sh --nat`. - For NAT changes, verify both guest outbound access and host rule cleanup, for example with `./verify.sh --nat`.

View file

@ -183,8 +183,9 @@ banger image build --name docker-dev --docker
``` ```
Rebuilt images install a pinned `mise` at `/usr/local/bin/mise`, activate it Rebuilt images install a pinned `mise` at `/usr/local/bin/mise`, activate it
for bash login and interactive shells, and install `opencode` through `mise` for bash login and interactive shells, install `opencode` through `mise`, and
by default. configure `tmux-resurrect` plus `tmux-continuum` for `root` with periodic
autosaves and manual-only restore by default.
Show or delete images: Show or delete images:
```bash ```bash

View file

@ -106,6 +106,13 @@ INSTALL_DOCKER=0
MISE_VERSION="v2025.12.0" MISE_VERSION="v2025.12.0"
MISE_INSTALL_PATH="/usr/local/bin/mise" MISE_INSTALL_PATH="/usr/local/bin/mise"
MISE_ACTIVATE_LINE='eval "$(/usr/local/bin/mise activate bash)"' MISE_ACTIVATE_LINE='eval "$(/usr/local/bin/mise activate bash)"'
TMUX_PLUGIN_DIR="/root/.tmux/plugins"
TMUX_RESURRECT_DIR="/root/.tmux/resurrect"
TMUX_TPM_REPO="https://github.com/tmux-plugins/tpm"
TMUX_RESURRECT_REPO="https://github.com/tmux-plugins/tmux-resurrect"
TMUX_CONTINUUM_REPO="https://github.com/tmux-plugins/tmux-continuum"
TMUX_MANAGED_START="# >>> banger tmux plugins >>>"
TMUX_MANAGED_END="# <<< banger tmux plugins <<<"
MODULES_DIR="$(bundle_path default_modules_dir "$RUNTIME_DIR/wtf/root/lib/modules/6.8.0-94-generic")" MODULES_DIR="$(bundle_path default_modules_dir "$RUNTIME_DIR/wtf/root/lib/modules/6.8.0-94-generic")"
PACKAGES_FILE="$(banger_packages_file)" PACKAGES_FILE="$(banger_packages_file)"
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
@ -413,9 +420,62 @@ if [[ \"$INSTALL_DOCKER\" == \"1\" ]]; then
systemctl enable --now docker || true systemctl enable --now docker || true
fi fi
fi fi
rm -f /root/get-docker /root/get-docker.sh /tmp/get-docker /tmp/get-docker.sh
git config --system init.defaultBranch main git config --system init.defaultBranch main
" "
log "configuring tmux resurrect"
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
"root@${GUEST_IP}" bash -se <<EOF
set -euo pipefail
install_tmux_plugin() {
local dir="\$1"
local repo="\$2"
if [[ -d "\$dir/.git" ]]; then
git -C "\$dir" fetch --depth 1 origin
git -C "\$dir" reset --hard FETCH_HEAD
else
rm -rf "\$dir"
git clone --depth 1 "\$repo" "\$dir"
fi
}
mkdir -p "$TMUX_PLUGIN_DIR" "$TMUX_RESURRECT_DIR"
install_tmux_plugin "$TMUX_PLUGIN_DIR/tpm" "$TMUX_TPM_REPO"
install_tmux_plugin "$TMUX_PLUGIN_DIR/tmux-resurrect" "$TMUX_RESURRECT_REPO"
install_tmux_plugin "$TMUX_PLUGIN_DIR/tmux-continuum" "$TMUX_CONTINUUM_REPO"
TMUX_CONF="/root/.tmux.conf"
tmp_tmux_conf="\$(mktemp)"
if [[ -f "\$TMUX_CONF" ]]; then
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"
else
: > "\$tmp_tmux_conf"
fi
if [[ -s "\$tmp_tmux_conf" ]]; then
printf '\n' >> "\$tmp_tmux_conf"
fi
cat >> "\$tmp_tmux_conf" <<'TMUXCONF'
$TMUX_MANAGED_START
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-resurrect'
set -g @plugin 'tmux-plugins/tmux-continuum'
set -g @continuum-save-interval '15'
set -g @continuum-restore 'off'
set -g @resurrect-dir '/root/.tmux/resurrect'
run '~/.tmux/plugins/tpm/tpm'
$TMUX_MANAGED_END
TMUXCONF
mv "\$tmp_tmux_conf" "\$TMUX_CONF"
chmod 0644 "\$TMUX_CONF"
EOF
if [[ -n "$MODULES_DIR" ]]; then if [[ -n "$MODULES_DIR" ]]; then
MODULES_BASE="$(basename "$MODULES_DIR")" MODULES_BASE="$(basename "$MODULES_DIR")"
log "copying kernel modules ($MODULES_BASE) into guest" log "copying kernel modules ($MODULES_BASE) into guest"

View file

@ -24,6 +24,13 @@ const (
defaultMiseInstallPath = "/usr/local/bin/mise" defaultMiseInstallPath = "/usr/local/bin/mise"
defaultMiseActivateLine = `eval "$(/usr/local/bin/mise activate bash)"` defaultMiseActivateLine = `eval "$(/usr/local/bin/mise activate bash)"`
defaultOpenCodeTool = "github:anomalyco/opencode" defaultOpenCodeTool = "github:anomalyco/opencode"
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 { type imageBuildSpec struct {
@ -242,6 +249,7 @@ func buildProvisionScript(vmName, dnsServer string, packages []string, installDo
fmt.Fprintf(&script, "PACKAGES=%s\n", shellArray(packages)) fmt.Fprintf(&script, "PACKAGES=%s\n", shellArray(packages))
script.WriteString("DEBIAN_FRONTEND=noninteractive apt-get -y install \"${PACKAGES[@]}\"\n") script.WriteString("DEBIAN_FRONTEND=noninteractive apt-get -y install \"${PACKAGES[@]}\"\n")
appendMiseSetup(&script) appendMiseSetup(&script)
appendTmuxSetup(&script)
if installDocker { if installDocker {
script.WriteString("DEBIAN_FRONTEND=noninteractive apt-get -y remove containerd || true\n") 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("if ! DEBIAN_FRONTEND=noninteractive apt-get -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; then\n")
@ -249,6 +257,7 @@ func buildProvisionScript(vmName, dnsServer string, packages []string, installDo
script.WriteString("fi\n") script.WriteString("fi\n")
script.WriteString("if command -v systemctl >/dev/null 2>&1; then systemctl enable --now docker || true; 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") script.WriteString("git config --system init.defaultBranch main\n")
return script.String() return script.String()
} }
@ -270,6 +279,54 @@ func appendMiseSetup(script *bytes.Buffer) {
appendLineIfMissing(script, "/etc/bash.bashrc", defaultMiseActivateLine) appendLineIfMissing(script, "/etc/bash.bashrc", defaultMiseActivateLine)
} }
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 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) { func appendLineIfMissing(script *bytes.Buffer, path, line string) {
fmt.Fprintf(script, "touch %s\n", shellQuote(path)) 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, "if ! grep -Fqx %s %s; then\n", shellQuote(line), shellQuote(path))

View file

@ -5,7 +5,7 @@ import (
"testing" "testing"
) )
func TestBuildProvisionScriptInstallsAndActivatesMise(t *testing.T) { func TestBuildProvisionScriptInstallsDefaultTools(t *testing.T) {
t.Parallel() t.Parallel()
script := buildProvisionScript("devbox", "1.1.1.1", []string{"git", "curl"}, false) script := buildProvisionScript("devbox", "1.1.1.1", []string{"git", "curl"}, false)
@ -16,6 +16,17 @@ func TestBuildProvisionScriptInstallsAndActivatesMise(t *testing.T) {
"if [ -n \"${BASH_VERSION:-}\" ] && [ -x '/usr/local/bin/mise' ]; then", "if [ -n \"${BASH_VERSION:-}\" ] && [ -x '/usr/local/bin/mise' ]; then",
`eval "$(/usr/local/bin/mise activate bash)"`, `eval "$(/usr/local/bin/mise activate bash)"`,
`if ! grep -Fqx 'eval "$(/usr/local/bin/mise activate bash)"' '/etc/bash.bashrc'; then`, `if ! grep -Fqx 'eval "$(/usr/local/bin/mise activate bash)"' '/etc/bash.bashrc'; then`,
`git clone --depth 1 'https://github.com/tmux-plugins/tpm' "$TMUX_PLUGIN_DIR/tpm"`,
`git clone --depth 1 'https://github.com/tmux-plugins/tmux-resurrect' "$TMUX_PLUGIN_DIR/tmux-resurrect"`,
`git clone --depth 1 'https://github.com/tmux-plugins/tmux-continuum' "$TMUX_PLUGIN_DIR/tmux-continuum"`,
"# >>> banger tmux plugins >>>",
"set -g @plugin 'tmux-plugins/tmux-resurrect'",
"set -g @plugin 'tmux-plugins/tmux-continuum'",
"set -g @continuum-save-interval '15'",
"set -g @continuum-restore 'off'",
"set -g @resurrect-dir '/root/.tmux/resurrect'",
"run '~/.tmux/plugins/tpm/tpm'",
"rm -f /root/get-docker /root/get-docker.sh /tmp/get-docker /tmp/get-docker.sh",
} { } {
if !strings.Contains(script, snippet) { 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)