Unverified Commit 3f2b28d0 authored by Minseong Choi's avatar Minseong Choi 💬
Browse files

fix(bootstrap): ship deploy/paper in the embedded game-stack tar

parent 587f1831
Loading
Loading
Loading
Loading
+6 −5
Changes for bootstrap_asset.go: 6 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -13,8 +13,8 @@ var bootstrapScript string
//go:embed deploy/crd/*.yaml
var bootstrapAssets embed.FS

// gameStackAssets carries everything deploy/bootstrap.sh needs to build the two
// always-on game images (login limbo + lobby) and the Velocity plugin, for the TUI
// gameStackAssets carries everything deploy/bootstrap.sh needs to build the three
// game images (login limbo, lobby, plain Paper) and the Velocity plugin, for the TUI
// install path — which pipes the embedded bootstrap.sh into bash and therefore has
// NO source checkout on disk to build from.
//
@@ -26,6 +26,7 @@ var bootstrapAssets embed.FS
//
//go:embed deploy/limbo/Dockerfile deploy/limbo/entrypoint.sh
//go:embed deploy/lobby/Dockerfile deploy/lobby/entrypoint.sh
//go:embed deploy/paper/Dockerfile deploy/paper/entrypoint.sh
//go:embed plugins/limbo/build.gradle plugins/limbo/settings.gradle plugins/limbo/src
//go:embed plugins/paper/build.gradle plugins/paper/settings.gradle plugins/paper/src
//go:embed plugins/velocity/build.gradle plugins/velocity/settings.gradle plugins/velocity/src
@@ -46,9 +47,9 @@ func GameStackTar(w io.Writer) error {
		if err != nil {
			return err
		}
		// Mode 0644 for everything: entrypoint.sh is invoked as `sh <file>` by both
		// Dockerfiles precisely because the +x bit does not survive a Windows checkout,
		// so nothing here needs to be executable.
		// Mode 0644 for everything: entrypoint.sh is invoked as `sh <file>` by all
		// three Dockerfiles precisely because the +x bit does not survive a Windows
		// checkout, so nothing here needs to be executable.
		if err := tw.WriteHeader(&tar.Header{
			Name:     path,
			Mode:     0o644,
+70 −0
Changes for bootstrap_asset_test.go: 70 added lines, 0 removed lines.
Original line number Diff line number Diff line
package felis

import (
	"io/fs"
	"regexp"
	"strings"
	"testing"
)
@@ -104,6 +106,74 @@ func TestBootstrapPinsViaBlockConnectionsOff(t *testing.T) {
	}
}

// The embed list and the images bootstrap.sh builds are two lists nobody reconciles.
// deploy/paper shipped an image build without ever being added to gameStackAssets, and
// nothing said so: a checkout on disk satisfies the build either way, and the tar is
// only the build context on the path that has no checkout — `curl | bash`, where the
// third `docker build -f` then names a file that was never unpacked. So derive the
// inputs from the script and from each Dockerfile's own COPY lines instead of restating
// them here; a fourth image inherits the check for free.
func TestGameStackTarCarriesEveryBuildInput(t *testing.T) {
	// Matches the path only when GAME_STACK_DIR is followed by one, which skips the
	// build-context arguments (`"$GAME_STACK_DIR"`, `"${GAME_STACK_DIR}:/src:z"`) and
	// the glob for gradle's output, none of which are inputs this tar has to carry.
	found := regexp.MustCompile(`\$\{GAME_STACK_DIR\}/(\S+?)"`).FindAllStringSubmatch(BootstrapScript(), -1)
	var paths []string
	seen := map[string]bool{}
	for _, m := range found {
		if !seen[m[1]] {
			seen[m[1]] = true
			paths = append(paths, m[1])
		}
	}
	// Guards the regex itself: a rewrite of how bootstrap.sh spells the build context
	// would otherwise turn this test into an unconditional pass. It has to come before
	// the loop — a missing file in there is fatal, and a floor placed after it would
	// never be reached to say that the regex, not the tar, is what went wrong.
	if len(paths) < 3 {
		t.Fatalf("only %d game-stack path(s) resolved out of bootstrap.sh; the limbo, "+
			"lobby and paper Dockerfiles are all built from ${GAME_STACK_DIR}", len(paths))
	}
	for _, path := range paths {
		requireEmbedded(t, path)
		// A Dockerfile that arrives without the files it COPYs fails just as late and
		// just as far from here; the deploy/paper gap was missing its entrypoint too.
		for _, src := range copySources(t, path) {
			requireEmbedded(t, src)
		}
	}
}

// copySources lists the build-context paths a Dockerfile COPYs in, skipping the
// --from=<stage> copies, whose sources are produced by an earlier stage rather than
// unpacked from the tar.
func copySources(t *testing.T, dockerfile string) []string {
	t.Helper()
	var out []string
	// Continuations are joined first: a COPY split across lines would otherwise be two
	// fragments, neither of them starting with COPY followed by a source, and its
	// source would slip past unchecked.
	body := strings.ReplaceAll(readGameStackFile(t, dockerfile), "\\\n", " ")
	for line := range strings.SplitSeq(body, "\n") {
		f := strings.Fields(line)
		if len(f) < 2 || f[0] != "COPY" || strings.HasPrefix(f[1], "--") {
			continue
		}
		out = append(out, strings.TrimSuffix(f[1], "/"))
	}
	return out
}

// fs.Stat rather than ReadFile: half of these are directories (`COPY plugins/shared/`),
// and embed.FS answers for those too.
func requireEmbedded(t *testing.T, path string) {
	t.Helper()
	if _, err := fs.Stat(gameStackAssets, path); err != nil {
		t.Errorf("%s is a game-stack build input but is not in gameStackAssets; an "+
			"install with no source checkout dies on it: %v", path, err)
	}
}

func readGameStackFile(t *testing.T, name string) string {
	t.Helper()
	b, err := gameStackAssets.ReadFile(name)