m/cli/metroctl: factor out some helper functions
We will need these to create metroctl-compatible configs from
//metropolis/cluster/launch.
Change-Id: I2705afefb62b7e1b35c87d9753c4ca9c7f534c26
Reviewed-on: https://review.monogon.dev/c/monogon/+/1324
Tested-by: Jenkins CI
Reviewed-by: Mateusz Zalega <mateusz@monogon.tech>
diff --git a/metropolis/cli/metroctl/BUILD.bazel b/metropolis/cli/metroctl/BUILD.bazel
index d35ba41..7af98f3 100644
--- a/metropolis/cli/metroctl/BUILD.bazel
+++ b/metropolis/cli/metroctl/BUILD.bazel
@@ -4,7 +4,6 @@
name = "metroctl_lib",
srcs = [
"approve.go",
- "credentials.go",
"describe.go",
"format.go",
"install.go",
@@ -37,8 +36,6 @@
"@com_github_spf13_cobra//:cobra",
"@io_k8s_apimachinery//pkg/apis/meta/v1:meta",
"@io_k8s_client_go//pkg/apis/clientauthentication/v1:clientauthentication",
- "@io_k8s_client_go//tools/clientcmd",
- "@io_k8s_client_go//tools/clientcmd/api",
"@org_golang_google_grpc//:go_default_library",
],
)
diff --git a/metropolis/cli/metroctl/core/BUILD.bazel b/metropolis/cli/metroctl/core/BUILD.bazel
index af948af..133ff2e 100644
--- a/metropolis/cli/metroctl/core/BUILD.bazel
+++ b/metropolis/cli/metroctl/core/BUILD.bazel
@@ -3,6 +3,7 @@
go_library(
name = "core",
srcs = [
+ "config.go",
"core.go",
"install.go",
"rpc.go",
@@ -18,6 +19,9 @@
"@com_github_diskfs_go_diskfs//disk",
"@com_github_diskfs_go_diskfs//filesystem",
"@com_github_diskfs_go_diskfs//partition/gpt",
+ "@io_k8s_client_go//pkg/apis/clientauthentication/v1:clientauthentication",
+ "@io_k8s_client_go//tools/clientcmd",
+ "@io_k8s_client_go//tools/clientcmd/api",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_google_protobuf//proto",
"@org_golang_x_net//proxy",
diff --git a/metropolis/cli/metroctl/core/config.go b/metropolis/cli/metroctl/core/config.go
new file mode 100644
index 0000000..d62bf2e
--- /dev/null
+++ b/metropolis/cli/metroctl/core/config.go
@@ -0,0 +1,185 @@
+package core
+
+import (
+ "crypto/ed25519"
+ "crypto/rand"
+ "crypto/x509"
+ "encoding/pem"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+
+ clientauthentication "k8s.io/client-go/pkg/apis/clientauthentication/v1"
+ "k8s.io/client-go/tools/clientcmd"
+ clientapi "k8s.io/client-go/tools/clientcmd/api"
+)
+
+const (
+ // OwnerKeyFileName is the filename of the owner key in a metroctl config
+ // directory.
+ OwnerKeyFileName = "owner-key.pem"
+ // OwnerCertificateFileName is the filename of the owner certificate in a
+ // metroctl config directory.
+ OwnerCertificateFileName = "owner.pem"
+)
+
+// NoCredentialsError indicates that the requested datum (eg. owner key or owner
+// certificate) is not present in the requested directory.
+var NoCredentialsError = errors.New("owner certificate or key does not exist")
+
+// A PEM block type for a Metropolis initial owner private key
+const ownerKeyType = "METROPOLIS INITIAL OWNER PRIVATE KEY"
+
+// GetOrMakeOwnerKey returns the owner key for a given metroctl configuration
+// directory path, generating and saving it first if it doesn't exist.
+func GetOrMakeOwnerKey(path string) (ed25519.PrivateKey, error) {
+ existing, err := GetOwnerKey(path)
+ switch err {
+ case nil:
+ return existing, nil
+ case NoCredentialsError:
+ default:
+ return nil, err
+ }
+
+ _, priv, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ return nil, fmt.Errorf("when generating key: %w", err)
+ }
+ if err := WriteOwnerKey(path, priv); err != nil {
+ return nil, err
+ }
+ return priv, nil
+}
+
+// WriteOwnerKey saves a given raw ED25519 private key as the owner key at a
+// given metroctl configuration directory path.
+func WriteOwnerKey(path string, priv ed25519.PrivateKey) error {
+ pemPriv := pem.EncodeToMemory(&pem.Block{Type: ownerKeyType, Bytes: priv})
+ if err := os.WriteFile(filepath.Join(path, OwnerKeyFileName), pemPriv, 0600); err != nil {
+ return fmt.Errorf("when saving key: %w", err)
+ }
+ return nil
+}
+
+// GetOwnerKey loads and returns a raw ED25519 private key from the saved owner
+// key in a given metroctl configuration directory path. If the owner key doesn't
+// exist, NoCredentialsError will be returned.
+func GetOwnerKey(path string) (ed25519.PrivateKey, error) {
+ ownerPrivateKeyPEM, err := os.ReadFile(filepath.Join(path, OwnerKeyFileName))
+ if os.IsNotExist(err) {
+ return nil, NoCredentialsError
+ } else if err != nil {
+ return nil, fmt.Errorf("failed to load owner private key: %w", err)
+ }
+ block, _ := pem.Decode(ownerPrivateKeyPEM)
+ if block == nil {
+ return nil, errors.New("owner-key.pem contains invalid PEM armoring")
+ }
+ if block.Type != ownerKeyType {
+ return nil, fmt.Errorf("owner-key.pem contains a PEM block that's not a %v", ownerKeyType)
+ }
+ if len(block.Bytes) != ed25519.PrivateKeySize {
+ return nil, errors.New("owner-key.pem contains a non-Ed25519 key")
+ }
+ return block.Bytes, nil
+}
+
+// WriteOwnerCertificate saves a given DER-encoded X509 certificate as the owner
+// key for a given metroctl configuration directory path.
+func WriteOwnerCertificate(path string, cert []byte) error {
+ ownerCertPEM := pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: cert,
+ }
+ if err := os.WriteFile(filepath.Join(path, OwnerCertificateFileName), pem.EncodeToMemory(&ownerCertPEM), 0644); err != nil {
+ return err
+ }
+ return nil
+}
+
+// GetOwnerCredentials loads and returns a raw ED25519 private key alongside a
+// DER-encoded X509 certificate from the saved owner key and certificate in a
+// given metroctl configuration directory path. If either the key or certificate
+// doesn't exist, NoCredentialsError will be returned.
+func GetOwnerCredentials(path string) (cert *x509.Certificate, key ed25519.PrivateKey, err error) {
+ key, err = GetOwnerKey(path)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ ownerCertPEM, err := os.ReadFile(filepath.Join(path, OwnerCertificateFileName))
+ if os.IsNotExist(err) {
+ return nil, nil, NoCredentialsError
+ } else if err != nil {
+ return nil, nil, fmt.Errorf("failed to load owner certificate: %w", err)
+ }
+ block, _ := pem.Decode(ownerCertPEM)
+ if block == nil {
+ return nil, nil, errors.New("owner.pem contains invalid PEM armoring")
+ }
+ if block.Type != "CERTIFICATE" {
+ return nil, nil, fmt.Errorf("owner.pem contains a PEM block that's not a CERTIFICATE")
+ }
+ cert, err = x509.ParseCertificate(block.Bytes)
+ if err != nil {
+ return nil, nil, fmt.Errorf("owner.pem contains an invalid X.509 certificate: %w", err)
+ }
+ return
+}
+
+// InstallK8SWrapper configures the current user's kubectl to connect to a
+// Kubernetes cluster as defined by server (Metropolis wrapped APIServer
+// endpoint), proxyURL (optional proxy URL) and metroctlPath (binary managing
+// credentials for this cluster, and used to implement the client-side part of
+// the Metropolis-wrapped APIServer protocol). The configuration will be saved to
+// the 'configName' context in kubectl.
+func InstallK8SWrapper(metroctlPath, configName, server, proxyURL string) error {
+ ca := clientcmd.NewDefaultPathOptions()
+ config, err := ca.GetStartingConfig()
+ if err != nil {
+ return fmt.Errorf("getting initial config failed: %w", err)
+ }
+
+ config.AuthInfos[configName] = &clientapi.AuthInfo{
+ Exec: &clientapi.ExecConfig{
+ APIVersion: clientauthentication.SchemeGroupVersion.String(),
+ Command: metroctlPath,
+ Args: []string{"k8scredplugin"},
+ 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[configName] = &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.
+ // TODO(issues/144): adjust cluster endpoints once have functioning roles
+ // implemented.
+ InsecureSkipTLSVerify: true,
+ Server: server,
+ ProxyURL: proxyURL,
+ }
+
+ config.Contexts[configName] = &clientapi.Context{
+ AuthInfo: configName,
+ Cluster: configName,
+ 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 = configName
+ }
+
+ if err := clientcmd.ModifyConfig(ca, *config, true); err != nil {
+ return fmt.Errorf("modifying config failed: %w", err)
+ }
+ return nil
+}
diff --git a/metropolis/cli/metroctl/credentials.go b/metropolis/cli/metroctl/credentials.go
deleted file mode 100644
index 2160bea..0000000
--- a/metropolis/cli/metroctl/credentials.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package main
-
-import (
- "crypto/ed25519"
- "crypto/x509"
- "encoding/pem"
- "errors"
- "fmt"
- "os"
- "path/filepath"
-)
-
-var noCredentialsError = errors.New("owner certificate or key does not exist")
-
-// getOwnerKey returns the cluster owner's key, if one exists, from the current
-// metroctl config directory.
-func getOwnerKey() (ed25519.PrivateKey, error) {
- ownerPrivateKeyPEM, err := os.ReadFile(filepath.Join(flags.configPath, "owner-key.pem"))
- if os.IsNotExist(err) {
- return nil, noCredentialsError
- } else if err != nil {
- return nil, fmt.Errorf("failed to load owner private key: %w", err)
- }
- block, _ := pem.Decode(ownerPrivateKeyPEM)
- if block == nil {
- return nil, errors.New("owner-key.pem contains invalid PEM armoring")
- }
- if block.Type != ownerKeyType {
- return nil, fmt.Errorf("owner-key.pem contains a PEM block that's not a %v", ownerKeyType)
- }
- if len(block.Bytes) != ed25519.PrivateKeySize {
- return nil, errors.New("owner-key.pem contains a non-Ed25519 key")
- }
- return block.Bytes, nil
-}
-
-// getCredentials returns Metropolis credentials (if any) from the current
-// metroctl config directory.
-func getCredentials() (cert *x509.Certificate, key ed25519.PrivateKey, err error) {
- key, err = getOwnerKey()
- if err != nil {
- return nil, nil, err
- }
-
- ownerCertPEM, err := os.ReadFile(filepath.Join(flags.configPath, "owner.pem"))
- if os.IsNotExist(err) {
- return nil, nil, noCredentialsError
- } else if err != nil {
- return nil, nil, fmt.Errorf("failed to load owner certificate: %w", err)
- }
- block, _ := pem.Decode(ownerCertPEM)
- if block == nil {
- return nil, nil, errors.New("owner.pem contains invalid PEM armoring")
- }
- if block.Type != "CERTIFICATE" {
- return nil, nil, fmt.Errorf("owner.pem contains a PEM block that's not a CERTIFICATE")
- }
- cert, err = x509.ParseCertificate(block.Bytes)
- if err != nil {
- return nil, nil, fmt.Errorf("owner.pem contains an invalid X.509 certificate: %w", err)
- }
- return
-}
diff --git a/metropolis/cli/metroctl/install.go b/metropolis/cli/metroctl/install.go
index 07faad4..e43e07c 100644
--- a/metropolis/cli/metroctl/install.go
+++ b/metropolis/cli/metroctl/install.go
@@ -4,13 +4,10 @@
"bytes"
"context"
"crypto/ed25519"
- "crypto/rand"
_ "embed"
- "encoding/pem"
"io"
"log"
"os"
- "path/filepath"
"github.com/spf13/cobra"
@@ -41,9 +38,6 @@
// the --endpoints flag.
var bootstrap bool
-// A PEM block type for a Metropolis initial owner private key
-const ownerKeyType = "METROPOLIS INITIAL OWNER PRIVATE KEY"
-
//go:embed metropolis/installer/kernel.efi
var installer []byte
@@ -81,39 +75,15 @@
var params *api.NodeParameters
if bootstrap {
- var ownerPublicKey ed25519.PublicKey
- ownerPrivateKeyPEM, err := os.ReadFile(filepath.Join(flags.configPath, "owner-key.pem"))
- if os.IsNotExist(err) {
- pub, priv, err := ed25519.GenerateKey(rand.Reader)
- if err != nil {
- log.Fatalf("Failed to generate owner private key: %v", err)
- }
- pemPriv := pem.EncodeToMemory(&pem.Block{Type: ownerKeyType, Bytes: priv})
- if err := os.WriteFile(filepath.Join(flags.configPath, "owner-key.pem"), pemPriv, 0600); err != nil {
- log.Fatalf("Failed to store owner private key: %v", err)
- }
- ownerPublicKey = pub
- } else if err != nil {
- log.Fatalf("Failed to load owner private key: %v", err)
- } else {
- 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)
- ownerPublicKey = ownerPrivateKey.Public().(ed25519.PublicKey)
+ priv, err := core.GetOrMakeOwnerKey(flags.configPath)
+ if err != nil {
+ log.Fatalf("Failed to generate or get owner key: %v", err)
}
-
+ pub := priv.Public().(ed25519.PublicKey)
params = &api.NodeParameters{
Cluster: &api.NodeParameters_ClusterBootstrap_{
ClusterBootstrap: &api.NodeParameters_ClusterBootstrap{
- OwnerPublicKey: ownerPublicKey,
+ OwnerPublicKey: pub,
},
},
}
diff --git a/metropolis/cli/metroctl/k8scredplugin.go b/metropolis/cli/metroctl/k8scredplugin.go
index e434a61..605bc87 100644
--- a/metropolis/cli/metroctl/k8scredplugin.go
+++ b/metropolis/cli/metroctl/k8scredplugin.go
@@ -10,6 +10,8 @@
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthentication "k8s.io/client-go/pkg/apis/clientauthentication/v1"
+
+ "source.monogon.dev/metropolis/cli/metroctl/core"
)
var k8scredpluginCmd = &cobra.Command{
@@ -23,8 +25,8 @@
}
func doK8sCredPlugin(cmd *cobra.Command, args []string) {
- cert, key, err := getCredentials()
- if err == noCredentialsError {
+ cert, key, err := core.GetOwnerCredentials(flags.configPath)
+ if err == core.NoCredentialsError {
log.Fatal("No credentials found on your machine")
}
if err != nil {
diff --git a/metropolis/cli/metroctl/rpc.go b/metropolis/cli/metroctl/rpc.go
index 9a8903a..8e8ee48 100644
--- a/metropolis/cli/metroctl/rpc.go
+++ b/metropolis/cli/metroctl/rpc.go
@@ -12,8 +12,8 @@
func dialAuthenticated(ctx context.Context) *grpc.ClientConn {
// Collect credentials, validate command parameters, and try dialing the
// cluster.
- ocert, opkey, err := getCredentials()
- if err == noCredentialsError {
+ ocert, opkey, err := core.GetOwnerCredentials(flags.configPath)
+ if err == core.NoCredentialsError {
log.Fatalf("You have to take ownership of the cluster first: %v", err)
}
if len(flags.clusterEndpoints) == 0 {
diff --git a/metropolis/cli/metroctl/takeownership.go b/metropolis/cli/metroctl/takeownership.go
index 6102cf1..e0b2f79 100644
--- a/metropolis/cli/metroctl/takeownership.go
+++ b/metropolis/cli/metroctl/takeownership.go
@@ -2,17 +2,12 @@
import (
"context"
- "encoding/pem"
"log"
"net"
"os"
"os/exec"
- "path/filepath"
"github.com/spf13/cobra"
- clientauthentication "k8s.io/client-go/pkg/apis/clientauthentication/v1"
- "k8s.io/client-go/tools/clientcmd"
- clientapi "k8s.io/client-go/tools/clientcmd/api"
"source.monogon.dev/metropolis/cli/metroctl/core"
clicontext "source.monogon.dev/metropolis/cli/pkg/context"
@@ -39,8 +34,8 @@
// Retrieve the cluster owner's private key, and use it to construct
// ephemeral credentials. Then, dial the cluster.
- opk, err := getOwnerKey()
- if err == noCredentialsError {
+ opk, err := core.GetOwnerKey(flags.configPath)
+ if err == core.NoCredentialsError {
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.")
}
if err != nil {
@@ -57,21 +52,12 @@
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 {
+ if err := core.WriteOwnerCertificate(flags.configPath, ownerCert.Certificate[0]); 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.
@@ -82,44 +68,11 @@
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.
- // TODO(issues/144): adjust cluster endpoints once have functioning roles
- // implemented.
- InsecureSkipTLSVerify: true,
- Server: "https://" + net.JoinHostPort(flags.clusterEndpoints[0], 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)
+ // TODO(issues/144): adjust cluster endpoints once have functioning roles
+ // implemented.
+ server := "https://" + net.JoinHostPort(flags.clusterEndpoints[0], node.KubernetesAPIWrappedPort.PortString())
+ if err := core.InstallK8SWrapper(metroctlPath, "metroctl", server, ""); err != nil {
+ log.Fatalf("Failed to install metroctl/k8s integration: %v", err)
}
log.Println("Success! kubeconfig is set up. You can now run kubectl --context=metropolis ... to access the Kubernetes cluster.")
}