blob: 975bd38b90819fe0d0b557a187a86443f2ab6815 [file] [log] [blame]
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)
}