a73x

internal/smoke/cloudinit_test.go

Ref:   Size: 5.5 KiB   History

package smoke

import (
	"strconv"
	"strings"
	"testing"

	"github.com/a73x/eitri/internal/cloudinit"
	"github.com/a73x/eitri/internal/server/api/client"
)

// grownRootBytes is what a guest reports once the seed's runcmd has grown its
// root to the disk the scenario asked for: the whole default disk, less the
// filesystem's own overhead, which is nowhere near half of it.
const grownRootBytes = client.DefaultDiskGB * (1 << 30)

// imageRootBytes is the root the default Ubuntu image ships — about 3.5G, the
// size an ungrown guest reports. Written as a literal because it is a fact
// about somebody else's image, not a number eitri gets to choose.
const imageRootBytes int64 = 3650722816

func TestGrownRootFloorSitsBetweenTheImageAndTheDefaultDisk(t *testing.T) {
	// The floor is derived from the default disk, so it moves when that moves.
	// What must stay true either way is that it separates the two roots a guest
	// can actually have: a floor under the image's root passes an ungrown guest,
	// and one above the disk fails every guest.
	if minGrownRootBytes <= imageRootBytes {
		t.Errorf("floor %d does not clear the image's own root %d — an ungrown guest would pass", minGrownRootBytes, imageRootBytes)
	}
	if minGrownRootBytes >= grownRootBytes {
		t.Errorf("floor %d is at or above the whole default disk %d — no guest could clear it", minGrownRootBytes, grownRootBytes)
	}
}

// probeOutput renders what seedProbeCmd prints in a guest, so each test states
// the three facts it is about and nothing else.
func probeOutput(rootBytes, writeFiles, runcmd string) string {
	return "root_bytes=" + rootBytes + "\n" +
		"byo_write_files=" + writeFiles + "\n" +
		"byo_runcmd=" + runcmd + "\n"
}

func TestSeedProbeAcceptsAGuestWhereBothSidesOfTheMergeLanded(t *testing.T) {
	out := probeOutput(strconv.FormatInt(grownRootBytes, 10), "tenant write_files ran", "tenant runcmd ran")
	if err := proveSeedSurvivedBYO(out); err != nil {
		t.Fatalf("proveSeedSurvivedBYO: %v", err)
	}
}

func TestSeedProbeRejectsARootTheGrowNeverReached(t *testing.T) {
	// The image's own ~3.5G root on a 10G disk: the seed's runcmd was replaced
	// by the tenant's, which is the half of the defect that costs disk.
	out := probeOutput(strconv.FormatInt(imageRootBytes, 10), "tenant write_files ran", "tenant runcmd ran")
	err := proveSeedSurvivedBYO(out)
	if err == nil {
		t.Fatal("proveSeedSurvivedBYO: want an error for an ungrown root, got nil")
	}
	if !strings.Contains(err.Error(), "runcmd") {
		t.Errorf("error = %q, want it to name the runcmd that was dropped", err)
	}
}

func TestSeedProbeRejectsAGuestThatLostTheTenantsWriteFiles(t *testing.T) {
	// The mirror image: eitri's lists won and the tenant's were discarded. A
	// merge that only ever keeps eitri's side is not a merge.
	out := probeOutput(strconv.FormatInt(grownRootBytes, 10), "", "tenant runcmd ran")
	err := proveSeedSurvivedBYO(out)
	if err == nil {
		t.Fatal("proveSeedSurvivedBYO: want an error when the tenant's write_files is gone, got nil")
	}
	if !strings.Contains(err.Error(), "write_files") {
		t.Errorf("error = %q, want it to name the tenant's write_files", err)
	}
}

func TestSeedProbeRejectsAGuestThatLostTheTenantsRuncmd(t *testing.T) {
	out := probeOutput(strconv.FormatInt(grownRootBytes, 10), "tenant write_files ran", "")
	err := proveSeedSurvivedBYO(out)
	if err == nil {
		t.Fatal("proveSeedSurvivedBYO: want an error when the tenant's runcmd is gone, got nil")
	}
	if !strings.Contains(err.Error(), "runcmd") {
		t.Errorf("error = %q, want it to name the tenant's runcmd", err)
	}
}

func TestSeedProbeRejectsOutputItCannotRead(t *testing.T) {
	// A probe whose root_bytes is missing or not a number proves nothing, and
	// must never be mistaken for a pass.
	for _, out := range []string{
		"",
		probeOutput("", "w", "r"),
		probeOutput("not-a-number", "w", "r"),
	} {
		if err := proveSeedSurvivedBYO(out); err == nil {
			t.Errorf("proveSeedSurvivedBYO(%q): want an error, got nil", out)
		}
	}
}

func TestBYOCloudInitCarriesTheTwoKeysThatUsedToClobberTheSeed(t *testing.T) {
	// The document is the whole point of the leg: without both keys the guest
	// never exercises the merge the seed's merge_how exists for.
	if !strings.Contains(byoCloudInit, "\nwrite_files:") {
		t.Error("byoCloudInit must carry a write_files key")
	}
	if !strings.Contains(byoCloudInit, "\nruncmd:") {
		t.Error("byoCloudInit must carry a runcmd key")
	}
	if !strings.HasPrefix(byoCloudInit, "#cloud-config\n") {
		t.Error("byoCloudInit must be a #cloud-config document")
	}
	for _, path := range []string{byoWriteFilesMarker, byoRuncmdMarker} {
		if !strings.Contains(byoCloudInit, path) {
			t.Errorf("byoCloudInit does not write %s, so the probe reads a file nothing creates", path)
		}
		if !strings.Contains(seedProbeCmd, path) {
			t.Errorf("seedProbeCmd does not read %s", path)
		}
	}
}

func TestBYOCloudInitSurvivesTheControlPlanesOwnSSHKeyMerge(t *testing.T) {
	// The create request carries an SSH key alongside the document, and the
	// control plane merges the one into the other before the seed ever sees it.
	// A document that merge refuses is a 400 at create, so the leg would fail
	// before it proved anything about cloud-init.
	merged, err := cloudinit.AddSSHKey(byoCloudInit, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAISMOKE smoke@gate")
	if err != nil {
		t.Fatalf("AddSSHKey(byoCloudInit): %v", err)
	}
	for _, want := range []string{"write_files", "runcmd", byoWriteFilesMarker, byoRuncmdMarker, "ssh_authorized_keys"} {
		if !strings.Contains(merged, want) {
			t.Errorf("merged document lost %q:\n%s", want, merged)
		}
	}
}