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