Bake mise into default VM images

New VMs should have mise available without a per-VM bootstrap step, and the activation needs to work in the default root bash workflow.

Install a pinned mise binary during both the Go-native image build path and the customize.sh rootfs rebuild path, then enable bash activation through /etc/profile.d for login shells and /etc/bash.bashrc for interactive shells.

Add a regression around the generated provisioning script and validate with bash -n customize.sh, go test ./..., and make build. Rebuilding the default rootfs is still required before future default-image VMs pick up the change.
This commit is contained in:
Thales Maciel 2026-03-18 13:13:11 -03:00
parent 7b7f7e676c
commit ff8482b841
No known key found for this signature in database
GPG key ID: 33112E6833C34679
3 changed files with 64 additions and 0 deletions

View file

@ -19,6 +19,12 @@ import (
"banger/internal/system"
)
const (
defaultMiseVersion = "v2025.12.0"
defaultMiseInstallPath = "/usr/local/bin/mise"
defaultMiseActivateLine = `eval "$(/usr/local/bin/mise activate bash)"`
)
type imageBuildSpec struct {
ID string
Name string
@ -234,6 +240,7 @@ func buildProvisionScript(vmName, dnsServer string, packages []string, installDo
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")
appendMiseSetup(&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")
@ -249,6 +256,25 @@ 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) {
fmt.Fprintf(script, "curl -fsSL https://mise.run | MISE_INSTALL_PATH=%s MISE_VERSION=%s sh\n", shellQuote(defaultMiseInstallPath), shellQuote(defaultMiseVersion))
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 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 {

View file

@ -0,0 +1,23 @@
package daemon
import (
"strings"
"testing"
)
func TestBuildProvisionScriptInstallsAndActivatesMise(t *testing.T) {
t.Parallel()
script := buildProvisionScript("devbox", "1.1.1.1", []string{"git", "curl"}, false)
for _, snippet := range []string{
"curl -fsSL https://mise.run | MISE_INSTALL_PATH='/usr/local/bin/mise' MISE_VERSION='v2025.12.0' sh",
"cat > /etc/profile.d/mise.sh <<'EOF'",
"if [ -n \"${BASH_VERSION:-}\" ] && [ -x '/usr/local/bin/mise' ]; then",
`eval "$(/usr/local/bin/mise activate bash)"`,
`if ! grep -Fqx 'eval "$(/usr/local/bin/mise activate bash)"' '/etc/bash.bashrc'; then`,
} {
if !strings.Contains(script, snippet) {
t.Fatalf("buildProvisionScript missing snippet %q\nscript:\n%s", snippet, script)
}
}
}