m: {cli,installer}: runfiles through datafile.MustGet

This implements datafile, a small library to more ergonomically resolve
Bazel runfiles:

1. It also works in cases where a tool doesn't run through `bazel run`.
2. It provides a MustGet wrapper which returns already read bytes and
   removes some boilerplate at the callsite.
3. It allows us to extend the library in the future to prepare special
   'self-contained' builds of some binaries, for example to bundle the
   installer kernel in metroctl.

We then use this library to simplify the installer and installer tests.
In the installer, we technically remove the ability to specify arbitrary
kernels/bundles on the command line, but is this functionality actually
useful?

Change-Id: I46155b9951729c810e0d36930b470edfdfd82943
Reviewed-on: https://review.monogon.dev/c/monogon/+/484
Reviewed-by: Mateusz Zalega <mateusz@monogon.tech>
diff --git a/metropolis/cli/pkg/datafile/BUILD.bazel b/metropolis/cli/pkg/datafile/BUILD.bazel
new file mode 100644
index 0000000..8628d9a
--- /dev/null
+++ b/metropolis/cli/pkg/datafile/BUILD.bazel
@@ -0,0 +1,9 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["datafile.go"],
+    importpath = "source.monogon.dev/metropolis/cli/pkg/datafile",
+    visibility = ["//visibility:public"],
+    deps = ["@io_bazel_rules_go//go/tools/bazel:go_default_library"],
+)
diff --git a/metropolis/cli/pkg/datafile/datafile.go b/metropolis/cli/pkg/datafile/datafile.go
new file mode 100644
index 0000000..a65780d
--- /dev/null
+++ b/metropolis/cli/pkg/datafile/datafile.go
@@ -0,0 +1,99 @@
+// datafile provides an abstraction for accessing files passed through the data
+// attribute in a Bazel build rule.
+//
+// It thinly wraps around the Bazel/Go runfile library (to allow running from
+// outside `bazel run`).
+package datafile
+
+import (
+	"bufio"
+	"fmt"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/bazelbuild/rules_go/go/tools/bazel"
+)
+
+// parseManifest takes a bazel runfile MANIFEST and parses it into a map from
+// workspace-relative path to absolute path, flattening all workspaces into a
+// single tree.
+func parseManifest(path string) (map[string]string, error) {
+	f, err := os.Open(path)
+	if err != nil {
+		return nil, fmt.Errorf("could not open MANIFEST: %v", err)
+	}
+	defer f.Close()
+
+	manifest := make(map[string]string)
+	scanner := bufio.NewScanner(f)
+	for scanner.Scan() {
+		parts := strings.Split(scanner.Text(), " ")
+		if len(parts) != 2 {
+			continue
+		}
+		fpathParts := strings.Split(parts[0], string(os.PathSeparator))
+		fpath := strings.Join(fpathParts[1:], string(os.PathSeparator))
+		manifest[fpath] = parts[1]
+	}
+	return manifest, nil
+}
+
+// resolveRunfile tries to resolve a workspace-relative file path into an
+// absolute path with the use of bazel runfiles, through either the original
+// Bazel/Go runfile integration or a wrapper that also supports running from
+// outside `bazel run`.
+func resolveRunfile(path string) (string, error) {
+	var errEx error
+	ep, err := os.Executable()
+	if err == nil {
+		rfdir := ep + ".runfiles"
+		mfpath := filepath.Join(rfdir, "MANIFEST")
+		if stat, err := os.Stat(rfdir); err == nil && stat.IsDir() {
+			// We have a runfiles directory, parse MANIFEST and resolve files this way.
+			manifest, err := parseManifest(mfpath)
+			if err == nil {
+				tpath := manifest[path]
+				if tpath == "" {
+					errEx = fmt.Errorf("not in MANIFEST")
+				} else {
+					return tpath, err
+				}
+			} else {
+				errEx = err
+			}
+		} else {
+			errEx = err
+		}
+	}
+
+	// Try runfiles just in case.
+	rf, errRF := bazel.Runfile(path)
+	if errRF == nil {
+		return rf, nil
+	}
+	return "", fmt.Errorf("could not resolve via executable location (%v) and runfile resolution failed: %v", errEx, errRF)
+}
+
+// Get tries to read a workspace-relative file path through the use of Bazel
+// runfiles, including for cases when executables are running outside `bazel
+// run`.
+func Get(path string) ([]byte, error) {
+	rfpath, err := resolveRunfile(path)
+	if err != nil {
+		return nil, err
+	}
+	return os.ReadFile(rfpath)
+}
+
+// MustGet either successfully resolves a file through Get() or logs an error
+// (through the stdlib log library) and stops execution. This should thus only
+// be used in binaries which use the log library.
+func MustGet(path string) []byte {
+	res, err := Get(path)
+	if err != nil {
+		log.Fatalf("Could not get datafile %s: %v", path, err)
+	}
+	return res
+}