| package main |
| |
| import ( |
| "context" |
| "crypto/ed25519" |
| "encoding/pem" |
| "log" |
| "net" |
| "os" |
| "os/exec" |
| "path/filepath" |
| |
| "github.com/spf13/cobra" |
| "google.golang.org/grpc" |
| clientauthentication "k8s.io/client-go/pkg/apis/clientauthentication/v1" |
| "k8s.io/client-go/tools/clientcmd" |
| clientapi "k8s.io/client-go/tools/clientcmd/api" |
| |
| clicontext "source.monogon.dev/metropolis/cli/pkg/context" |
| "source.monogon.dev/metropolis/node" |
| "source.monogon.dev/metropolis/node/core/rpc" |
| 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: cobra.ExactArgs(0), |
| Run: doTakeOwnership, |
| } |
| |
| func doTakeOwnership(cmd *cobra.Command, _ []string) { |
| if len(flags.clusterEndpoints) != 1 { |
| log.Fatalf("takeownership requires a single cluster endpoint to be provided with the --endpoints parameter.") |
| } |
| clusterEp := flags.clusterEndpoints[0] |
| |
| ctx := clicontext.WithInterrupt(context.Background()) |
| ownerPrivateKeyPEM, err := os.ReadFile(filepath.Join(flags.configPath, "owner-key.pem")) |
| if os.IsNotExist(err) { |
| log.Fatalf("Owner key does not exist. takeownership needs to be executed on the same system that has previously installed the cluster using metroctl install.") |
| } else if err != nil { |
| log.Fatalf("Failed to load owner private key: %v", err) |
| } |
| block, _ := pem.Decode(ownerPrivateKeyPEM) |
| if block == nil { |
| log.Fatalf("owner-key.pem contains invalid PEM") |
| } |
| if block.Type != ownerKeyType { |
| log.Fatalf("owner-key.pem contains a PEM block that's not a %v", ownerKeyType) |
| } |
| if len(block.Bytes) != ed25519.PrivateKeySize { |
| log.Fatal("owner-key.pem contains non-Ed25519 key") |
| } |
| ownerPrivateKey := ed25519.PrivateKey(block.Bytes) |
| |
| ephCreds, err := rpc.NewEphemeralCredentials(ownerPrivateKey, nil) |
| if err != nil { |
| log.Fatalf("Failed to create ephemeral credentials: %v", err) |
| } |
| client, err := grpc.Dial(net.JoinHostPort(clusterEp, node.CuratorServicePort.PortString()), grpc.WithTransportCredentials(ephCreds)) |
| if err != nil { |
| log.Fatalf("Failed to create client to given node address: %v", err) |
| } |
| defer client.Close() |
| aaa := apb.NewAAAClient(client) |
| ownerCert, err := rpc.RetrieveOwnerCertificate(ctx, aaa, ownerPrivateKey) |
| if err != nil { |
| log.Fatalf("Failed to retrive owner certificate from cluster: %v", err) |
| } |
| ownerCertPEM := pem.Block{ |
| Type: "CERTIFICATE", |
| Bytes: ownerCert.Certificate[0], |
| } |
| if err := os.WriteFile(filepath.Join(flags.configPath, "owner.pem"), pem.EncodeToMemory(&ownerCertPEM), 0644); err != nil { |
| log.Printf("Failed to store retrieved owner certificate: %v", err) |
| log.Fatalln("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...") |
| |
| ca := clientcmd.NewDefaultPathOptions() |
| config, err := ca.GetStartingConfig() |
| if err != nil { |
| log.Fatalf("Failed to get initial kubeconfig to add Metropolis cluster: %v", err) |
| } |
| // 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 { |
| log.Fatalf("Failed to create kubectl entry as metroctl is neither in PATH nor can its absolute path be determined: %v", err) |
| } |
| } |
| |
| config.AuthInfos["metropolis"] = &clientapi.AuthInfo{ |
| Exec: &clientapi.ExecConfig{ |
| APIVersion: clientauthentication.SchemeGroupVersion.String(), |
| Command: metroctlPath, |
| Args: []string{k8scredpluginCmd.Use}, |
| InstallHint: `Authenticating to Metropolis clusters requires metroctl to be present. |
| Running metroctl takeownership creates this entry and either points to metroctl as a command in |
| PATH if metroctl is in PATH at that time or to the absolute path to metroctl at that time. |
| If you moved metroctl afterwards or want to switch to PATH resolution, edit $HOME/.kube/config and |
| change users.metropolis.exec.command to the required path (or just metroctl if using PATH resolution).`, |
| InteractiveMode: clientapi.NeverExecInteractiveMode, |
| }, |
| } |
| |
| config.Clusters["metropolis"] = &clientapi.Cluster{ |
| // MVP: This is insecure, but making this work would be wasted effort |
| // as all of it will be replaced by the identity system. |
| InsecureSkipTLSVerify: true, |
| Server: "https://" + net.JoinHostPort(clusterEp, node.KubernetesAPIWrappedPort.PortString()), |
| } |
| |
| config.Contexts["metropolis"] = &clientapi.Context{ |
| AuthInfo: "metropolis", |
| Cluster: "metropolis", |
| Namespace: "default", |
| } |
| |
| // Only set us as the current context if no other exists. Changing that |
| // unprompted would be kind of rude. |
| if config.CurrentContext == "" { |
| config.CurrentContext = "metropolis" |
| } |
| |
| if err := clientcmd.ModifyConfig(ca, *config, true); err != nil { |
| log.Fatalf("Failed to modify kubeconfig to add Metropolis cluster: %v", err) |
| } |
| log.Println("Success! kubeconfig is set up. You can now run kubectl --context=metropolis ... to access the Kubernetes cluster.") |
| } |
| |
| func init() { |
| rootCmd.AddCommand(takeownershipCommand) |
| } |