blob: 975bd38b90819fe0d0b557a187a86443f2ab6815 [file] [log] [blame]
Lorenz Bruna9b455f2021-12-07 03:53:22 +01001package main
2
3import (
4 "context"
5 "crypto/ed25519"
6 "encoding/pem"
7 "log"
8 "net"
9 "os"
Lorenz Brun20d1dd12022-07-01 12:21:42 +000010 "os/exec"
Lorenz Bruna9b455f2021-12-07 03:53:22 +010011 "path/filepath"
12
Lorenz Bruna9b455f2021-12-07 03:53:22 +010013 "github.com/spf13/cobra"
Serge Bazanski399ce552022-03-29 12:52:42 +020014 "google.golang.org/grpc"
Lorenz Brun20d1dd12022-07-01 12:21:42 +000015 clientauthentication "k8s.io/client-go/pkg/apis/clientauthentication/v1"
16 "k8s.io/client-go/tools/clientcmd"
17 clientapi "k8s.io/client-go/tools/clientcmd/api"
Lorenz Bruna9b455f2021-12-07 03:53:22 +010018
Lorenz Bruna9b455f2021-12-07 03:53:22 +010019 clicontext "source.monogon.dev/metropolis/cli/pkg/context"
20 "source.monogon.dev/metropolis/node"
21 "source.monogon.dev/metropolis/node/core/rpc"
Serge Bazanskidc1bec42021-12-16 17:38:53 +010022 apb "source.monogon.dev/metropolis/proto/api"
Lorenz Bruna9b455f2021-12-07 03:53:22 +010023)
24
25var takeownershipCommand = &cobra.Command{
Mateusz Zalegab1e7ee42022-07-08 12:19:02 +020026 Use: "takeownership",
Lorenz Bruna9b455f2021-12-07 03:53:22 +010027 Short: "Takes ownership of a new Metropolis cluster",
28 Long: `This takes ownership of a new Metropolis cluster by asking the new
29cluster to issue an owner certificate to for the owner key generated by a
Mateusz Zalegab1e7ee42022-07-08 12:19:02 +020030previous invocation of metroctl install on this machine. A single cluster
31endpoint must be provided with the --endpoints parameter.`,
32 Args: cobra.ExactArgs(0),
33 Run: doTakeOwnership,
Lorenz Bruna9b455f2021-12-07 03:53:22 +010034}
35
Mateusz Zalegab1e7ee42022-07-08 12:19:02 +020036func doTakeOwnership(cmd *cobra.Command, _ []string) {
37 if len(flags.clusterEndpoints) != 1 {
38 log.Fatalf("takeownership requires a single cluster endpoint to be provided with the --endpoints parameter.")
39 }
40 clusterEp := flags.clusterEndpoints[0]
41
Lorenz Bruna9b455f2021-12-07 03:53:22 +010042 ctx := clicontext.WithInterrupt(context.Background())
Mateusz Zalega8234c162022-07-08 17:05:50 +020043 ownerPrivateKeyPEM, err := os.ReadFile(filepath.Join(flags.configPath, "owner-key.pem"))
Lorenz Bruna9b455f2021-12-07 03:53:22 +010044 if os.IsNotExist(err) {
45 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.")
46 } else if err != nil {
47 log.Fatalf("Failed to load owner private key: %v", err)
48 }
49 block, _ := pem.Decode(ownerPrivateKeyPEM)
50 if block == nil {
51 log.Fatalf("owner-key.pem contains invalid PEM")
52 }
53 if block.Type != ownerKeyType {
54 log.Fatalf("owner-key.pem contains a PEM block that's not a %v", ownerKeyType)
55 }
56 if len(block.Bytes) != ed25519.PrivateKeySize {
57 log.Fatal("owner-key.pem contains non-Ed25519 key")
58 }
59 ownerPrivateKey := ed25519.PrivateKey(block.Bytes)
60
Serge Bazanski399ce552022-03-29 12:52:42 +020061 ephCreds, err := rpc.NewEphemeralCredentials(ownerPrivateKey, nil)
62 if err != nil {
63 log.Fatalf("Failed to create ephemeral credentials: %v", err)
64 }
Mateusz Zalegab1e7ee42022-07-08 12:19:02 +020065 client, err := grpc.Dial(net.JoinHostPort(clusterEp, node.CuratorServicePort.PortString()), grpc.WithTransportCredentials(ephCreds))
Lorenz Bruna9b455f2021-12-07 03:53:22 +010066 if err != nil {
67 log.Fatalf("Failed to create client to given node address: %v", err)
68 }
69 defer client.Close()
70 aaa := apb.NewAAAClient(client)
71 ownerCert, err := rpc.RetrieveOwnerCertificate(ctx, aaa, ownerPrivateKey)
72 if err != nil {
73 log.Fatalf("Failed to retrive owner certificate from cluster: %v", err)
74 }
75 ownerCertPEM := pem.Block{
76 Type: "CERTIFICATE",
77 Bytes: ownerCert.Certificate[0],
78 }
Mateusz Zalega8234c162022-07-08 17:05:50 +020079 if err := os.WriteFile(filepath.Join(flags.configPath, "owner.pem"), pem.EncodeToMemory(&ownerCertPEM), 0644); err != nil {
Lorenz Bruna9b455f2021-12-07 03:53:22 +010080 log.Printf("Failed to store retrieved owner certificate: %v", err)
81 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.")
82 }
Lorenz Brun20d1dd12022-07-01 12:21:42 +000083 log.Print("Successfully retrieved owner credentials! You now own this cluster. Setting up kubeconfig now...")
84
85 ca := clientcmd.NewDefaultPathOptions()
86 config, err := ca.GetStartingConfig()
87 if err != nil {
88 log.Fatalf("Failed to get initial kubeconfig to add Metropolis cluster: %v", err)
89 }
90 // If the user has metroctl in their path, use the metroctl from path as
91 // a credential plugin. Otherwise use the path to the currently-running
92 // metroctl.
93 metroctlPath := "metroctl"
94 if _, err := exec.LookPath("metroctl"); err != nil {
95 metroctlPath, err = os.Executable()
96 if err != nil {
97 log.Fatalf("Failed to create kubectl entry as metroctl is neither in PATH nor can its absolute path be determined: %v", err)
98 }
99 }
100
101 config.AuthInfos["metropolis"] = &clientapi.AuthInfo{
102 Exec: &clientapi.ExecConfig{
103 APIVersion: clientauthentication.SchemeGroupVersion.String(),
104 Command: metroctlPath,
105 Args: []string{k8scredpluginCmd.Use},
106 InstallHint: `Authenticating to Metropolis clusters requires metroctl to be present.
107Running metroctl takeownership creates this entry and either points to metroctl as a command in
108PATH if metroctl is in PATH at that time or to the absolute path to metroctl at that time.
109If you moved metroctl afterwards or want to switch to PATH resolution, edit $HOME/.kube/config and
110change users.metropolis.exec.command to the required path (or just metroctl if using PATH resolution).`,
111 InteractiveMode: clientapi.NeverExecInteractiveMode,
112 },
113 }
114
115 config.Clusters["metropolis"] = &clientapi.Cluster{
116 // MVP: This is insecure, but making this work would be wasted effort
117 // as all of it will be replaced by the identity system.
118 InsecureSkipTLSVerify: true,
Mateusz Zalegab1e7ee42022-07-08 12:19:02 +0200119 Server: "https://" + net.JoinHostPort(clusterEp, node.KubernetesAPIWrappedPort.PortString()),
Lorenz Brun20d1dd12022-07-01 12:21:42 +0000120 }
121
122 config.Contexts["metropolis"] = &clientapi.Context{
123 AuthInfo: "metropolis",
124 Cluster: "metropolis",
125 Namespace: "default",
126 }
127
128 // Only set us as the current context if no other exists. Changing that
129 // unprompted would be kind of rude.
130 if config.CurrentContext == "" {
131 config.CurrentContext = "metropolis"
132 }
133
134 if err := clientcmd.ModifyConfig(ca, *config, true); err != nil {
135 log.Fatalf("Failed to modify kubeconfig to add Metropolis cluster: %v", err)
136 }
137 log.Println("Success! kubeconfig is set up. You can now run kubectl --context=metropolis ... to access the Kubernetes cluster.")
Lorenz Bruna9b455f2021-12-07 03:53:22 +0100138}
139
140func init() {
141 rootCmd.AddCommand(takeownershipCommand)
142}