metropolis/cli/metroctl: move takeownership under cluster subcommand tree
This is in preparation for having more cluster-wide commands (same as we
keep 'node' for all node-specific commands).
Change-Id: I3fa5935e1d751261bf82a80cf61e811fa36effc9
Reviewed-on: https://review.monogon.dev/c/monogon/+/3593
Tested-by: Jenkins CI
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/metropolis/cli/metroctl/cmd_cluster_takeownership.go b/metropolis/cli/metroctl/cmd_cluster_takeownership.go
new file mode 100644
index 0000000..068dd3b
--- /dev/null
+++ b/metropolis/cli/metroctl/cmd_cluster_takeownership.go
@@ -0,0 +1,105 @@
+package main
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log"
+ "os"
+ "os/exec"
+ "os/signal"
+
+ "github.com/spf13/cobra"
+ "google.golang.org/grpc"
+
+ "source.monogon.dev/metropolis/cli/metroctl/core"
+ "source.monogon.dev/metropolis/node/core/rpc"
+ "source.monogon.dev/metropolis/node/core/rpc/resolver"
+ apb "source.monogon.dev/metropolis/proto/api"
+)
+
+var takeownershipCommand = &cobra.Command{
+ Use: "takeownership",
+ Short: "Takes ownership of a new Metropolis cluster",
+ Long: `This takes ownership of a new Metropolis cluster by asking the new
+cluster to issue an owner certificate to for the owner key generated by a
+previous invocation of metroctl install on this machine. A single cluster
+endpoint must be provided with the --endpoints parameter.`,
+ Args: PrintUsageOnWrongArgs(cobra.ExactArgs(0)),
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
+ if len(flags.clusterEndpoints) != 1 {
+ return fmt.Errorf("takeownership requires a single cluster endpoint to be provided with the --endpoints parameter")
+ }
+
+ contextName, err := cmd.Flags().GetString("context")
+ if err != nil || contextName == "" {
+ return fmt.Errorf("takeownership requires a valid context name to be provided with the --context parameter")
+ }
+
+ ca, err := core.GetClusterCAWithTOFU(ctx, connectOptions())
+ if err != nil {
+ return fmt.Errorf("could not retrieve cluster CA: %w", err)
+ }
+
+ // Retrieve the cluster owner's private key, and use it to construct
+ // ephemeral credentials. Then, dial the cluster.
+ opk, err := core.GetOwnerKey(flags.configPath)
+ if errors.Is(err, core.ErrNoCredentials) {
+ return fmt.Errorf("owner key does not exist. takeownership needs to be executed on the same system that has previously installed the cluster using metroctl install")
+ }
+ if err != nil {
+ return fmt.Errorf("couldn't get owner's key: %w", err)
+ }
+ opts, err := core.DialOpts(ctx, connectOptions())
+ if err != nil {
+ return fmt.Errorf("while configuring cluster dial opts: %w", err)
+ }
+ creds, err := rpc.NewEphemeralCredentials(opk, rpc.WantRemoteCluster(ca))
+ if err != nil {
+ return fmt.Errorf("while generating ephemeral credentials: %w", err)
+ }
+ opts = append(opts, grpc.WithTransportCredentials(creds))
+
+ cc, err := grpc.Dial(resolver.MetropolisControlAddress, opts...)
+ if err != nil {
+ return fmt.Errorf("while dialing the cluster: %w", err)
+ }
+ aaa := apb.NewAAAClient(cc)
+
+ ownerCert, err := rpc.RetrieveOwnerCertificate(ctx, aaa, opk)
+ if err != nil {
+ return fmt.Errorf("failed to retrive owner certificate from cluster: %w", err)
+ }
+
+ if err := core.WriteOwnerCertificate(flags.configPath, ownerCert.Certificate[0]); err != nil {
+ log.Printf("Failed to store retrieved owner certificate: %v", err)
+ return fmt.Errorf("sorry, the cluster has been lost as taking ownership cannot be repeated. Fix the reason the file couldn't be written and reinstall the node")
+ }
+ log.Print("Successfully retrieved owner credentials! You now own this cluster. Setting up kubeconfig now...")
+
+ // If the user has metroctl in their path, use the metroctl from path as
+ // a credential plugin. Otherwise use the path to the currently-running
+ // metroctl.
+ metroctlPath := "metroctl"
+ if _, err := exec.LookPath("metroctl"); err != nil {
+ metroctlPath, err = os.Executable()
+ if err != nil {
+ return fmt.Errorf("failed to create kubectl entry as metroctl is neither in PATH nor can its absolute path be determined: %w", err)
+ }
+ }
+ // TODO(q3k, issues/144): this only works as long as all nodes are kubernetes controller
+ // nodes. This won't be the case for too long. Figure this out.
+ configName := "metroctl"
+ if err := core.InstallKubeletConfig(ctx, metroctlPath, connectOptions(), configName, flags.clusterEndpoints[0]); err != nil {
+ return fmt.Errorf("failed to install metroctl/k8s integration: %w", err)
+ }
+ log.Printf("Success! kubeconfig is set up. You can now run kubectl --context=%s ... to access the Kubernetes cluster.", configName)
+ return nil
+ },
+}
+
+func init() {
+ takeownershipCommand.Flags().String("context", "metroctl", "The name for the kubernetes context to configure")
+ clusterCmd.AddCommand(takeownershipCommand)
+}