Loading bootstrap_asset.go +6 −5 Changes for bootstrap_asset.go: 6 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -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. // Loading @@ -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 Loading @@ -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, Loading bootstrap_asset_test.go +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" ) Loading Loading @@ -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) Loading Loading
bootstrap_asset.go +6 −5 Changes for bootstrap_asset.go: 6 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -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. // Loading @@ -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 Loading @@ -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, Loading
bootstrap_asset_test.go +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" ) Loading Loading @@ -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) Loading