m/c/metroctl: embed installer and add argument for bundle

This embeds the installer in metroctl for easier distribution and adds
an option for passing a bundle path explicitly.

For the MVP this allows users to download metroctl and a bundle
separately and make an installer out of them.

Change-Id: I00f481c30a2e843d336248aad0dc4030d03c4576
Reviewed-on: https://review.monogon.dev/c/monogon/+/821
Tested-by: Jenkins CI
Reviewed-by: Mateusz Zalega <mateusz@monogon.tech>
diff --git a/metropolis/cli/metroctl/install.go b/metropolis/cli/metroctl/install.go
index c314fad..7ea36e3 100644
--- a/metropolis/cli/metroctl/install.go
+++ b/metropolis/cli/metroctl/install.go
@@ -4,7 +4,9 @@
 	"bytes"
 	"crypto/ed25519"
 	"crypto/rand"
+	_ "embed"
 	"encoding/pem"
+	"io"
 	"log"
 	"os"
 	"path/filepath"
@@ -18,14 +20,16 @@
 )
 
 var installCmd = &cobra.Command{
-	Short: "Contains subcommands to install Metropolis over different mediums.",
+	Short: "Contains subcommands to install Metropolis via different media.",
 	Use:   "install",
 }
 
+var bundlePath = installCmd.PersistentFlags().StringP("bundle", "b", "", "Path to the Metropolis bundle to be installed")
+
 var genusbCmd = &cobra.Command{
 	Use:     "genusb target",
 	Short:   "Generates a Metropolis installer disk or image.",
-	Example: "metroctl install genusb /dev/sdx",
+	Example: "metroctl install --bundle=metropolis-v0.1.zip genusb /dev/sdx",
 	Args:    cobra.ExactArgs(1), // One positional argument: the target
 	Run:     doGenUSB,
 }
@@ -33,9 +37,33 @@
 // A PEM block type for a Metropolis initial owner private key
 const ownerKeyType = "METROPOLIS INITIAL OWNER PRIVATE KEY"
 
+//go:embed metropolis/installer/kernel.efi
+var installer []byte
+
 func doGenUSB(cmd *cobra.Command, args []string) {
-	installer := datafile.MustGet("metropolis/installer/kernel.efi")
-	bundle := datafile.MustGet("metropolis/node/node.zip")
+	var bundleReader io.Reader
+	var bundleSize uint64
+	if bundlePath == nil || *bundlePath == "" {
+		// Attempt Bazel runfile bundle if not explicitly set
+		bundle, err := datafile.Get("metropolis/node/bundle.zip")
+		if err != nil {
+			log.Fatalf("No bundle specified and fallback to runfiles failed: %v", err)
+		}
+		bundleReader = bytes.NewReader(bundle)
+		bundleSize = uint64(len(bundle))
+	} else {
+		// Load bundle from specified path
+		bundle, err := os.Open(*bundlePath)
+		if err != nil {
+			log.Fatalf("Failed to open specified bundle: %v", err)
+		}
+		bundleStat, err := bundle.Stat()
+		if err != nil {
+			log.Fatalf("Failed to stat specified bundle: %v", err)
+		}
+		bundleReader = bundle
+		bundleSize = uint64(bundleStat.Size())
+	}
 
 	// TODO(lorenz): Have a key management story for this
 	if err := os.MkdirAll(filepath.Join(xdg.ConfigHome, "metroctl"), 0700); err != nil {
@@ -83,11 +111,11 @@
 
 	installerImageArgs := core.MakeInstallerImageArgs{
 		TargetPath:    args[0],
-		Installer:     bytes.NewBuffer(installer),
+		Installer:     bytes.NewReader(installer),
 		InstallerSize: uint64(len(installer)),
 		NodeParams:    params,
-		Bundle:        bytes.NewBuffer(bundle),
-		BundleSize:    uint64(len(bundle)),
+		Bundle:        bundleReader,
+		BundleSize:    bundleSize,
 	}
 
 	log.Printf("Generating installer image (this can take a while, see issues/92).")