m/c/metroctl: add prototext node-params option

This adds the node-params flag to metroctl install, which allows
advanced users to specify customized NodeParameters in a prototext file.
The cluster field gets overwritten by the existing logic.  As of now,
this is only useful for specifying network_config.

Change-Id: Ieccf208177bb49635a634e484c67e99e80792bda
Reviewed-on: https://review.monogon.dev/c/monogon/+/3700
Vouch-Run-CI: Lorenz Brun <lorenz@monogon.tech>
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
Tested-by: Jenkins CI
diff --git a/metropolis/cli/metroctl/cmd_install.go b/metropolis/cli/metroctl/cmd_install.go
index c40c9ed..9a996b2 100644
--- a/metropolis/cli/metroctl/cmd_install.go
+++ b/metropolis/cli/metroctl/cmd_install.go
@@ -11,6 +11,7 @@
 
 	"github.com/bazelbuild/rules_go/go/runfiles"
 	"github.com/spf13/cobra"
+	"google.golang.org/protobuf/encoding/prototext"
 
 	"source.monogon.dev/metropolis/proto/api"
 	cpb "source.monogon.dev/metropolis/proto/common"
@@ -35,6 +36,7 @@
 var bootstrapTPMMode = flagdefs.TPMModePflag(installCmd.PersistentFlags(), "bootstrap-tpm-mode", cpb.ClusterConfiguration_TPM_MODE_REQUIRED, "TPM mode to set on cluster")
 var bootstrapStorageSecurityPolicy = flagdefs.StorageSecurityPolicyPflag(installCmd.PersistentFlags(), "bootstrap-storage-security", cpb.ClusterConfiguration_STORAGE_SECURITY_POLICY_NEEDS_ENCRYPTION_AND_AUTHENTICATION, "Storage security policy to set on cluster")
 var bundlePath = installCmd.PersistentFlags().StringP("bundle", "b", "", "Path to the Metropolis bundle to be installed")
+var nodeParamPath = installCmd.PersistentFlags().String("node-params", "", "Path to the metropolis.proto.api.NodeParameters prototext file (advanced usage only)")
 
 func makeNodeParams() (*api.NodeParameters, error) {
 	ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
@@ -44,6 +46,18 @@
 	}
 
 	var params *api.NodeParameters
+	if *nodeParamPath != "" {
+		nodeParamsRaw, err := os.ReadFile(*nodeParamPath)
+		if err != nil {
+			return nil, fmt.Errorf("failed to read node-params file: %w", err)
+		}
+		if err := prototext.Unmarshal(nodeParamsRaw, params); err != nil {
+			return nil, fmt.Errorf("failed to parse node-params: %w", err)
+		}
+	} else {
+		params = &api.NodeParameters{}
+	}
+
 	if *bootstrap {
 		if flags.cluster == "" {
 			return nil, fmt.Errorf("when bootstrapping a cluster, the --cluster parameter is required")
@@ -58,15 +72,13 @@
 			return nil, fmt.Errorf("failed to generate or get owner key: %w", err)
 		}
 		pub := priv.Public().(ed25519.PublicKey)
-		params = &api.NodeParameters{
-			Cluster: &api.NodeParameters_ClusterBootstrap_{
-				ClusterBootstrap: &api.NodeParameters_ClusterBootstrap{
-					OwnerPublicKey: pub,
-					InitialClusterConfiguration: &cpb.ClusterConfiguration{
-						ClusterDomain:         flags.cluster,
-						StorageSecurityPolicy: *bootstrapStorageSecurityPolicy,
-						TpmMode:               *bootstrapTPMMode,
-					},
+		params.Cluster = &api.NodeParameters_ClusterBootstrap_{
+			ClusterBootstrap: &api.NodeParameters_ClusterBootstrap{
+				OwnerPublicKey: pub,
+				InitialClusterConfiguration: &cpb.ClusterConfiguration{
+					ClusterDomain:         flags.cluster,
+					StorageSecurityPolicy: *bootstrapStorageSecurityPolicy,
+					TpmMode:               *bootstrapTPMMode,
 				},
 			},
 		}
@@ -85,13 +97,11 @@
 			return nil, fmt.Errorf("while receiving cluster directory: %w", err)
 		}
 
-		params = &api.NodeParameters{
-			Cluster: &api.NodeParameters_ClusterRegister_{
-				ClusterRegister: &api.NodeParameters_ClusterRegister{
-					RegisterTicket:   resT.Ticket,
-					ClusterDirectory: resI.ClusterDirectory,
-					CaCertificate:    resI.CaCertificate,
-				},
+		params.Cluster = &api.NodeParameters_ClusterRegister_{
+			ClusterRegister: &api.NodeParameters_ClusterRegister{
+				RegisterTicket:   resT.Ticket,
+				ClusterDirectory: resI.ClusterDirectory,
+				CaCertificate:    resI.CaCertificate,
 			},
 		}
 	}