| // Copyright The Monogon Project Authors. |
| // SPDX-License-Identifier: Apache-2.0 |
| |
| package main |
| |
| import ( |
| "context" |
| "crypto/ed25519" |
| _ "embed" |
| "fmt" |
| "os" |
| "os/signal" |
| |
| "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" |
| |
| "source.monogon.dev/metropolis/cli/flagdefs" |
| "source.monogon.dev/metropolis/cli/metroctl/core" |
| common "source.monogon.dev/metropolis/node" |
| "source.monogon.dev/osbase/structfs" |
| ) |
| |
| var installCmd = &cobra.Command{ |
| Short: "Contains subcommands to install Metropolis via different media.", |
| Use: "install", |
| } |
| |
| // bootstrap is a flag controlling node parameters included in the installer |
| // image. If set, the installed node will bootstrap a new cluster. Otherwise, |
| // it will try to connect to the cluster which endpoints were provided with |
| // the --endpoints flag. |
| var bootstrap = installCmd.PersistentFlags().Bool("bootstrap", false, "Create a bootstrap installer image.") |
| 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 imagePath = installCmd.PersistentFlags().StringP("image", "", "", "Path to the OCI layout directory containing the Metropolis OS image 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) |
| |
| if err := os.MkdirAll(flags.configPath, 0700); err != nil && !os.IsExist(err) { |
| return nil, fmt.Errorf("failed to create config directory: %w", err) |
| } |
| |
| 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, ¶ms); 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") |
| } |
| if err := common.ValidateClusterDomain(flags.cluster); err != nil { |
| return nil, fmt.Errorf("invalid cluster domain: %w", err) |
| } |
| |
| // TODO(lorenz): Have a key management story for this |
| priv, err := core.GetOrMakeOwnerKey(flags.configPath) |
| if err != nil { |
| return nil, fmt.Errorf("failed to generate or get owner key: %w", err) |
| } |
| pub := priv.Public().(ed25519.PublicKey) |
| params.Cluster = &api.NodeParameters_ClusterBootstrap_{ |
| ClusterBootstrap: &api.NodeParameters_ClusterBootstrap{ |
| OwnerPublicKey: pub, |
| InitialClusterConfiguration: &cpb.ClusterConfiguration{ |
| ClusterDomain: flags.cluster, |
| StorageSecurityPolicy: *bootstrapStorageSecurityPolicy, |
| TpmMode: *bootstrapTPMMode, |
| }, |
| }, |
| } |
| } else { |
| cc, err := newAuthenticatedClient(ctx) |
| if err != nil { |
| return nil, fmt.Errorf("while creating client: %w", err) |
| } |
| mgmt := api.NewManagementClient(cc) |
| resT, err := mgmt.GetRegisterTicket(ctx, &api.GetRegisterTicketRequest{}) |
| if err != nil { |
| return nil, fmt.Errorf("while receiving register ticket: %w", err) |
| } |
| resI, err := mgmt.GetClusterInfo(ctx, &api.GetClusterInfoRequest{}) |
| if err != nil { |
| return nil, fmt.Errorf("while receiving cluster directory: %w", err) |
| } |
| |
| params.Cluster = &api.NodeParameters_ClusterRegister_{ |
| ClusterRegister: &api.NodeParameters_ClusterRegister{ |
| RegisterTicket: resT.Ticket, |
| ClusterDirectory: resI.ClusterDirectory, |
| CaCertificate: resI.CaCertificate, |
| }, |
| } |
| } |
| return ¶ms, nil |
| } |
| |
| func external(name, datafilePath string, flag *string) (string, error) { |
| if flag == nil || *flag == "" { |
| rPath, err := runfiles.Rlocation(datafilePath) |
| if err != nil { |
| return "", fmt.Errorf("no %s specified", name) |
| } |
| return rPath, nil |
| } |
| return *flag, nil |
| } |
| |
| func externalFile(name, datafilePath string, flag *string) (structfs.Blob, error) { |
| path, err := external(name, datafilePath, flag) |
| if err != nil { |
| return nil, err |
| } |
| f, err := structfs.OSPathBlob(path) |
| if err != nil { |
| return nil, fmt.Errorf("failed to open specified %s: %w", name, err) |
| } |
| return f, nil |
| } |
| |
| func init() { |
| rootCmd.AddCommand(installCmd) |
| } |