diff --git a/internal/cloudinit/cloudinit.go b/internal/cloudinit/cloudinit.go index c137b8a..5a0bd36 100644 --- a/internal/cloudinit/cloudinit.go +++ b/internal/cloudinit/cloudinit.go @@ -103,7 +103,10 @@ func extraPackages(osName string) string { func userData(v *config.VM, pubkey string, recipeBodies []string) (string, error) { base := fmt.Sprintf(userDataTemplate, guestShell(v.OS), pubkey, consolePasswordBlock(v.ConsolePassword)) - docs := []string{base, mountsDoc(v)} + docs := []string{base} + if m := mountsDoc(v); m != "" { + docs = append(docs, m) + } if extra := extraPackages(v.OS); extra != "" { docs = append(docs, extra) } @@ -116,11 +119,20 @@ func userData(v *config.VM, pubkey string, recipeBodies []string) (string, error // QEMU command line with nothing to mount them, so the share silently did // nothing. // -// nofail is required: some cloud kernels ship no 9p module (Debian's does -// not), and without nofail an unmountable share holds up boot. The host -// mount is ro, matching what QEMU enforces, so a write fails immediately +// debian's cloud kernel (deb13-cloud) ships no 9p module, so the mount can +// never succeed and cloud-init's mounts module marks the whole seed as +// errored on every boot. Skip the mounts for debian; the 9p share does not +// work there. +// ponytail: keyed on the one bundled image without 9p. Add another OS here if +// the catalog gains a second 9p-less image. +// +// nofail keeps a share that drops out at runtime from holding up boot. The +// host mount is ro, matching what QEMU enforces, so a write fails immediately // instead of after a remount that appears to succeed. func mountsDoc(v *config.VM) string { + if v.OS == "debian" { + return "" + } const opts = "trans=virtio,version=9p2000.L,%s,_netdev,nofail" var b strings.Builder b.WriteString("#cloud-config\nmounts:\n") diff --git a/internal/cloudinit/cloudinit_test.go b/internal/cloudinit/cloudinit_test.go index e7166a3..1150385 100644 --- a/internal/cloudinit/cloudinit_test.go +++ b/internal/cloudinit/cloudinit_test.go @@ -17,10 +17,11 @@ import ( // tests can inspect individual documents instead of grepping the whole // rendered file. -// withoutMounts returns the archive's documents minus the 9p mounts document, -// and fails if there isn't exactly one. Every VM gets a mounts document, so -// tests about the base and recipe documents filter it out instead of counting -// around it. +// withoutMounts returns the archive's documents minus the 9p mounts document. +// Every VM except debian gets one mounts document; debian's cloud kernel has +// no 9p module, so it gets none. The helper accepts zero or one and fails on +// more, so tests about the base and recipe documents filter it out instead of +// counting around it. func withoutMounts(t *testing.T, ud string) []archiveDoc { t.Helper() var rest []archiveDoc @@ -32,8 +33,8 @@ func withoutMounts(t *testing.T, ud string) []archiveDoc { } rest = append(rest, d) } - if found != 1 { - t.Fatalf("want exactly one mounts document, got %d:\n%s", found, ud) + if found > 1 { + t.Fatalf("want at most one mounts document, got %d:\n%s", found, ud) } return rest } @@ -445,6 +446,27 @@ func TestSeedInstallsSudoWhereItIsMissing(t *testing.T) { } } +// debian's cloud kernel has no 9p module, so the seed omits the mounts +// document. A present-but-unmountable 9p entry made cloud-init report the +// whole seed as errored on every boot. A 9p-capable OS still gets the mounts. +func TestSeedSkipsMountsOnDebian(t *testing.T) { + deb, err := userData(&config.VM{Name: "vm", OS: "debian"}, testPubkey, nil) + if err != nil { + t.Fatal(err) + } + if strings.Contains(deb, "mounts:") { + t.Errorf("debian seed carries a 9p mounts document:\n%s", deb) + } + + arch, err := userData(&config.VM{Name: "vm", OS: "arch"}, testPubkey, nil) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(arch, "mounts:") { + t.Errorf("arch seed is missing its 9p mounts document:\n%s", arch) + } +} + // Recipe fragments must still merge after the base block, whatever the OS: // this is the existing contract and the reason the cloud-config-archive // exists (see buildArchive). diff --git a/internal/recipes/bundled/docker/install-fedora.sh b/internal/recipes/bundled/docker/install-fedora.sh index dd4321b..7c281fd 100755 --- a/internal/recipes/bundled/docker/install-fedora.sh +++ b/internal/recipes/bundled/docker/install-fedora.sh @@ -3,9 +3,10 @@ # Fedora VM. set -e -# Add Docker's official repository -dnf -y install dnf-plugins-core -dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo +# dnf5 (Fedora 41+) dropped `config-manager --add-repo`. Write the repo file +# directly; that works on both dnf4 and dnf5. +curl -fsSL https://download.docker.com/linux/fedora/docker-ce.repo \ + -o /etc/yum.repos.d/docker-ce.repo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin