From 5e141fe98e08b5ccc34ea073e3d5b2310be81a73 Mon Sep 17 00:00:00 2001 From: Hussein Galal Date: Thu, 11 Jan 2024 00:57:46 +0200 Subject: [PATCH] Add kubeconfig subcommand (#87) * Add kubeconfig subcommand Signed-off-by: galal-hussein * Add kubeconfig subcommand Signed-off-by: galal-hussein * Add kubeconfig subcommand Signed-off-by: galal-hussein * Add kubeconfig subcommand Signed-off-by: galal-hussein --------- Signed-off-by: galal-hussein --- cli/cmds/cluster/cluster.go | 6 +- cli/cmds/cluster/create.go | 96 +--------- cli/cmds/kubeconfig/kubeconfig.go | 169 ++++++++++++++++++ cli/main.go | 4 +- pkg/controller/cluster/cluster.go | 14 +- .../{kubeconfig.go => bootstrap/bootstrap.go} | 123 ++----------- pkg/controller/cluster/server/ingress.go | 4 +- pkg/controller/cluster/server/service.go | 8 +- pkg/controller/kubeconfig/certs.go | 71 ++++++++ pkg/controller/kubeconfig/kubeconfig.go | 108 +++++++++++ pkg/controller/util/util.go | 2 + 11 files changed, 392 insertions(+), 213 deletions(-) create mode 100644 cli/cmds/kubeconfig/kubeconfig.go rename pkg/controller/cluster/server/{kubeconfig.go => bootstrap/bootstrap.go} (50%) create mode 100644 pkg/controller/kubeconfig/certs.go create mode 100644 pkg/controller/kubeconfig/kubeconfig.go diff --git a/cli/cmds/cluster/cluster.go b/cli/cmds/cluster/cluster.go index d2a00b2e..88e822bf 100644 --- a/cli/cmds/cluster/cluster.go +++ b/cli/cmds/cluster/cluster.go @@ -5,7 +5,7 @@ import ( "github.com/urfave/cli" ) -var clusterSubcommands = []cli.Command{ +var subcommands = []cli.Command{ { Name: "create", Usage: "Create new cluster", @@ -16,10 +16,10 @@ var clusterSubcommands = []cli.Command{ }, } -func NewClusterCommand() cli.Command { +func NewCommand() cli.Command { return cli.Command{ Name: "cluster", Usage: "cluster command", - Subcommands: clusterSubcommands, + Subcommands: subcommands, } } diff --git a/cli/cmds/cluster/create.go b/cli/cmds/cluster/create.go index 0621a027..55f1802d 100644 --- a/cli/cmds/cluster/create.go +++ b/cli/cmds/cluster/create.go @@ -3,7 +3,6 @@ package cluster import ( "context" "errors" - "fmt" "net/url" "os" "path/filepath" @@ -14,19 +13,17 @@ import ( "github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1" "github.com/rancher/k3k/pkg/controller/cluster" "github.com/rancher/k3k/pkg/controller/cluster/server" + "github.com/rancher/k3k/pkg/controller/kubeconfig" "github.com/rancher/k3k/pkg/controller/util" "github.com/sirupsen/logrus" "github.com/urfave/cli" - v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apiserver/pkg/authentication/user" clientgoscheme "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" - clientcmdapi "k8s.io/client-go/tools/clientcmd/api" "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -174,11 +171,16 @@ func createCluster(clx *cli.Context) error { } logrus.Infof("Extracting Kubeconfig for [%s] cluster", name) + cfg := &kubeconfig.KubeConfig{ + CN: util.AdminCommonName, + ORG: []string{user.SystemPrivilegedGroup}, + ExpiryDate: 0, + } + logrus.Infof("waiting for cluster to be available..") var kubeconfig []byte if err := retry.OnError(backoff, apierrors.IsNotFound, func() error { - kubeconfig, err = extractKubeconfig(ctx, ctrlClient, cluster, host[0]) + kubeconfig, err = cfg.Extract(ctx, ctrlClient, cluster, host[0]) if err != nil { - logrus.Infof("waiting for cluster to be available: %v", err) return err } return nil @@ -250,83 +252,3 @@ func newCluster(name, token string, servers, agents int32, clusterCIDR, serviceC }, } } - -func extractKubeconfig(ctx context.Context, client client.Client, cluster *v1alpha1.Cluster, serverIP string) ([]byte, error) { - nn := types.NamespacedName{ - Name: cluster.Name + "-kubeconfig", - Namespace: util.ClusterNamespace(cluster), - } - - var kubeSecret v1.Secret - if err := client.Get(ctx, nn, &kubeSecret); err != nil { - return nil, err - } - - kubeconfig := kubeSecret.Data["kubeconfig.yaml"] - if kubeconfig == nil { - return nil, errors.New("empty kubeconfig") - } - - nn = types.NamespacedName{ - Name: "k3k-server-service", - Namespace: util.ClusterNamespace(cluster), - } - - var k3kService v1.Service - if err := client.Get(ctx, nn, &k3kService); err != nil { - return nil, err - } - - if k3kService.Spec.Type == v1.ServiceTypeNodePort { - nodePort := k3kService.Spec.Ports[0].NodePort - - restConfig, err := clientcmd.RESTConfigFromKubeConfig(kubeconfig) - if err != nil { - return nil, err - } - hostURL := fmt.Sprintf("https://%s:%d", serverIP, nodePort) - restConfig.Host = hostURL - - clientConfig := generateKubeconfigFromRest(restConfig) - - b, err := clientcmd.Write(clientConfig) - if err != nil { - return nil, err - } - kubeconfig = b - } - - return kubeconfig, nil -} - -func generateKubeconfigFromRest(config *rest.Config) clientcmdapi.Config { - clusters := make(map[string]*clientcmdapi.Cluster) - clusters["default-cluster"] = &clientcmdapi.Cluster{ - Server: config.Host, - CertificateAuthorityData: config.CAData, - } - - contexts := make(map[string]*clientcmdapi.Context) - contexts["default-context"] = &clientcmdapi.Context{ - Cluster: "default-cluster", - Namespace: "default", - AuthInfo: "default", - } - - authinfos := make(map[string]*clientcmdapi.AuthInfo) - authinfos["default"] = &clientcmdapi.AuthInfo{ - ClientCertificateData: config.CertData, - ClientKeyData: config.KeyData, - } - - clientConfig := clientcmdapi.Config{ - Kind: "Config", - APIVersion: "v1", - Clusters: clusters, - Contexts: contexts, - CurrentContext: "default-context", - AuthInfos: authinfos, - } - - return clientConfig -} diff --git a/cli/cmds/kubeconfig/kubeconfig.go b/cli/cmds/kubeconfig/kubeconfig.go new file mode 100644 index 00000000..6c6e1228 --- /dev/null +++ b/cli/cmds/kubeconfig/kubeconfig.go @@ -0,0 +1,169 @@ +package kubeconfig + +import ( + "context" + "net/url" + "os" + "path/filepath" + "strings" + "time" + + "github.com/rancher/k3k/cli/cmds" + "github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1" + "github.com/rancher/k3k/pkg/controller/kubeconfig" + "github.com/rancher/k3k/pkg/controller/util" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apiserver/pkg/authentication/user" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func init() { + _ = clientgoscheme.AddToScheme(Scheme) + _ = v1alpha1.AddToScheme(Scheme) +} + +var ( + Scheme = runtime.NewScheme() + name string + cn string + org cli.StringSlice + altNames cli.StringSlice + expirationDays int64 + configName string + backoff = wait.Backoff{ + Steps: 5, + Duration: 20 * time.Second, + Factor: 2, + Jitter: 0.1, + } + generateKubeconfigFlags = []cli.Flag{ + cli.StringFlag{ + Name: "name", + Usage: "cluster name", + Destination: &name, + }, + cli.StringFlag{ + Name: "config-name", + Usage: "the name of the generated kubeconfig file", + Destination: &configName, + }, + cli.StringFlag{ + Name: "cn", + Usage: "Common name (CN) of the generated certificates for the kubeconfig", + Destination: &cn, + Value: util.AdminCommonName, + }, + cli.StringSliceFlag{ + Name: "org", + Usage: "Organization name (ORG) of the generated certificates for the kubeconfig", + Value: &org, + }, + cli.StringSliceFlag{ + Name: "altNames", + Usage: "altNames of the generated certificates for the kubeconfig", + Value: &altNames, + }, + cli.Int64Flag{ + Name: "expiration-days", + Usage: "Expiration date of the certificates used for the kubeconfig", + Destination: &expirationDays, + Value: 356, + }, + } +) + +var subcommands = []cli.Command{ + { + Name: "generate", + Usage: "Generate kubeconfig for clusters", + SkipFlagParsing: false, + SkipArgReorder: true, + Action: generate, + Flags: append(cmds.CommonFlags, generateKubeconfigFlags...), + }, +} + +func NewCommand() cli.Command { + return cli.Command{ + Name: "kubeconfig", + Usage: "Manage kubeconfig for clusters", + Subcommands: subcommands, + } +} + +func generate(clx *cli.Context) error { + var cluster v1alpha1.Cluster + ctx := context.Background() + + restConfig, err := clientcmd.BuildConfigFromFlags("", cmds.Kubeconfig) + if err != nil { + return err + } + + ctrlClient, err := client.New(restConfig, client.Options{ + Scheme: Scheme, + }) + if err != nil { + return err + } + + clusterKey := types.NamespacedName{ + Name: name, + } + + if err := ctrlClient.Get(ctx, clusterKey, &cluster); err != nil { + return err + } + + url, err := url.Parse(restConfig.Host) + if err != nil { + return err + } + host := strings.Split(url.Host, ":") + + certAltNames := kubeconfig.AddSANs(altNames) + if org == nil { + org = cli.StringSlice{user.SystemPrivilegedGroup} + } + cfg := kubeconfig.KubeConfig{ + CN: cn, + ORG: org, + ExpiryDate: time.Hour * 24 * time.Duration(expirationDays), + AltNames: certAltNames, + } + logrus.Infof("waiting for cluster to be available..") + var kubeconfig []byte + if err := retry.OnError(backoff, apierrors.IsNotFound, func() error { + kubeconfig, err = cfg.Extract(ctx, ctrlClient, &cluster, host[0]) + if err != nil { + return err + } + return nil + }); err != nil { + return err + } + pwd, err := os.Getwd() + if err != nil { + return err + } + + if configName == "" { + configName = cluster.Name + "-kubeconfig.yaml" + } + + logrus.Infof(`You can start using the cluster with: + + export KUBECONFIG=%s + kubectl cluster-info + `, filepath.Join(pwd, configName)) + + return os.WriteFile(configName, kubeconfig, 0644) +} diff --git a/cli/main.go b/cli/main.go index 59b9e476..94399554 100644 --- a/cli/main.go +++ b/cli/main.go @@ -5,6 +5,7 @@ import ( "github.com/rancher/k3k/cli/cmds" "github.com/rancher/k3k/cli/cmds/cluster" + "github.com/rancher/k3k/cli/cmds/kubeconfig" "github.com/sirupsen/logrus" "github.com/urfave/cli" ) @@ -18,7 +19,8 @@ const ( func main() { app := cmds.NewApp() app.Commands = []cli.Command{ - cluster.NewClusterCommand(), + cluster.NewCommand(), + kubeconfig.NewCommand(), } app.Version = version + " (" + gitCommit + ")" diff --git a/pkg/controller/cluster/cluster.go b/pkg/controller/cluster/cluster.go index 36099c57..597a344d 100644 --- a/pkg/controller/cluster/cluster.go +++ b/pkg/controller/cluster/cluster.go @@ -15,6 +15,8 @@ import ( "github.com/rancher/k3k/pkg/controller/cluster/agent" "github.com/rancher/k3k/pkg/controller/cluster/config" "github.com/rancher/k3k/pkg/controller/cluster/server" + "github.com/rancher/k3k/pkg/controller/cluster/server/bootstrap" + "github.com/rancher/k3k/pkg/controller/kubeconfig" "github.com/rancher/k3k/pkg/controller/util" "github.com/sirupsen/logrus" "go.etcd.io/etcd/api/v3/v3rpc/rpctypes" @@ -244,12 +246,12 @@ func (c *ClusterReconciler) createCluster(ctx context.Context, cluster *v1alpha1 } } - kubeconfigSecret, err := s.GenerateNewKubeConfig(ctx, serviceIP) + bootstrapSecret, err := bootstrap.Generate(ctx, cluster, serviceIP) if err != nil { return util.LogAndReturnErr("failed to generate new kubeconfig", err) } - if err := c.Client.Create(ctx, kubeconfigSecret); err != nil { + if err := c.Client.Create(ctx, bootstrapSecret); err != nil { if !apierrors.IsAlreadyExists(err) { return util.LogAndReturnErr("failed to create kubeconfig secret", err) } @@ -540,18 +542,18 @@ func (c *ClusterReconciler) getETCDTLS(cluster *v1alpha1.Cluster) (*tls.Config, klog.Infof("generating etcd TLS client certificate for cluster [%s]", cluster.Name) token := cluster.Spec.Token endpoint := "k3k-server-service." + util.ClusterNamespace(cluster) - var bootstrap *server.ControlRuntimeBootstrap + var b *bootstrap.ControlRuntimeBootstrap if err := retry.OnError(retry.DefaultBackoff, func(err error) bool { return true }, func() error { var err error - bootstrap, err = server.DecodedBootstrap(token, endpoint) + b, err = bootstrap.DecodedBootstrap(token, endpoint) return err }); err != nil { return nil, err } - etcdCert, etcdKey, err := server.CreateClientCertKey("etcd-client", nil, nil, []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, bootstrap.ETCDServerCA.Content, bootstrap.ETCDServerCAKey.Content) + etcdCert, etcdKey, err := kubeconfig.CreateClientCertKey("etcd-client", nil, nil, []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, 0, b.ETCDServerCA.Content, b.ETCDServerCAKey.Content) if err != nil { return nil, err } @@ -560,7 +562,7 @@ func (c *ClusterReconciler) getETCDTLS(cluster *v1alpha1.Cluster) (*tls.Config, return nil, err } // create rootCA CertPool - cert, err := certutil.ParseCertsPEM([]byte(bootstrap.ETCDServerCA.Content)) + cert, err := certutil.ParseCertsPEM([]byte(b.ETCDServerCA.Content)) if err != nil { return nil, err } diff --git a/pkg/controller/cluster/server/kubeconfig.go b/pkg/controller/cluster/server/bootstrap/bootstrap.go similarity index 50% rename from pkg/controller/cluster/server/kubeconfig.go rename to pkg/controller/cluster/server/bootstrap/bootstrap.go index 12946ae9..a764b848 100644 --- a/pkg/controller/cluster/server/kubeconfig.go +++ b/pkg/controller/cluster/server/bootstrap/bootstrap.go @@ -1,34 +1,22 @@ -package server +package bootstrap import ( "context" - "crypto" "crypto/tls" - "crypto/x509" "encoding/base64" "encoding/json" - "fmt" "net/http" "time" - certutil "github.com/rancher/dynamiclistener/cert" - "github.com/rancher/k3k/pkg/controller/util" + "github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apiserver/pkg/authentication/user" - "k8s.io/client-go/tools/clientcmd" - clientcmdapi "k8s.io/client-go/tools/clientcmd/api" "k8s.io/client-go/util/retry" ) -const ( - adminCommonName = "system:admin" - port = 6443 -) - type ControlRuntimeBootstrap struct { - ServerCA content - ServerCAKey content + ServerCA content `json:"serverCA"` + ServerCAKey content `json:"server"` ClientCA content ClientCAKey content ETCDServerCA content @@ -40,13 +28,11 @@ type content struct { Content string } -// GenerateNewKubeConfig generates the kubeconfig for the cluster: +// Generate generates the bootstrap for the cluster: // 1- use the server token to get the bootstrap data from k3s -// 2- generate client admin cert/key -// 3- use the ca cert from the bootstrap data & admin cert/key to write a new kubeconfig -// 4- save the new kubeconfig as a secret -func (s *Server) GenerateNewKubeConfig(ctx context.Context, ip string) (*v1.Secret, error) { - token := s.cluster.Spec.Token +// 2- save the bootstrap data as a secret +func Generate(ctx context.Context, cluster *v1alpha1.Cluster, ip string) (*v1.Secret, error) { + token := cluster.Spec.Token var bootstrap *ControlRuntimeBootstrap if err := retry.OnError(retry.DefaultBackoff, func(err error) bool { @@ -63,32 +49,21 @@ func (s *Server) GenerateNewKubeConfig(ctx context.Context, ip string) (*v1.Secr return nil, err } - adminCert, adminKey, err := CreateClientCertKey( - adminCommonName, []string{user.SystemPrivilegedGroup}, - nil, []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, - bootstrap.ClientCA.Content, - bootstrap.ClientCAKey.Content) + bootstrapData, err := json.Marshal(bootstrap) if err != nil { return nil, err } - - url := fmt.Sprintf("https://%s:%d", ip, port) - kubeconfigData, err := kubeconfig(url, []byte(bootstrap.ServerCA.Content), adminCert, adminKey) - if err != nil { - return nil, err - } - return &v1.Secret{ TypeMeta: metav1.TypeMeta{ Kind: "Secret", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ - Name: s.cluster.Name + "-kubeconfig", - Namespace: util.ClusterNamespace(s.cluster), + Name: cluster.Name + "-bootstrap", + Namespace: "k3k-" + cluster.Name, }, Data: map[string][]byte{ - "kubeconfig.yaml": kubeconfigData, + "bootstrap": bootstrapData, }, }, nil @@ -126,80 +101,6 @@ func requestBootstrap(token, serverIP string) (*ControlRuntimeBootstrap, error) return &runtimeBootstrap, nil } -func CreateClientCertKey(commonName string, organization []string, altNames *certutil.AltNames, extKeyUsage []x509.ExtKeyUsage, caCert, caKey string) ([]byte, []byte, error) { - caKeyPEM, err := certutil.ParsePrivateKeyPEM([]byte(caKey)) - if err != nil { - return nil, nil, err - } - - caCertPEM, err := certutil.ParseCertsPEM([]byte(caCert)) - if err != nil { - return nil, nil, err - } - - b, err := generateKey() - if err != nil { - return nil, nil, err - } - - key, err := certutil.ParsePrivateKeyPEM(b) - if err != nil { - return nil, nil, err - } - - cfg := certutil.Config{ - CommonName: commonName, - Organization: organization, - Usages: extKeyUsage, - } - if altNames != nil { - cfg.AltNames = *altNames - } - cert, err := certutil.NewSignedCert(cfg, key.(crypto.Signer), caCertPEM[0], caKeyPEM.(crypto.Signer)) - if err != nil { - return nil, nil, err - } - - return append(certutil.EncodeCertPEM(cert), certutil.EncodeCertPEM(caCertPEM[0])...), b, nil -} - -func generateKey() (data []byte, err error) { - generatedData, err := certutil.MakeEllipticPrivateKeyPEM() - if err != nil { - return nil, fmt.Errorf("error generating key: %v", err) - } - - return generatedData, nil -} - -func kubeconfig(url string, serverCA, clientCert, clientKey []byte) ([]byte, error) { - config := clientcmdapi.NewConfig() - - cluster := clientcmdapi.NewCluster() - cluster.CertificateAuthorityData = serverCA - cluster.Server = url - - authInfo := clientcmdapi.NewAuthInfo() - authInfo.ClientCertificateData = clientCert - authInfo.ClientKeyData = clientKey - - context := clientcmdapi.NewContext() - context.AuthInfo = "default" - context.Cluster = "default" - - config.Clusters["default"] = cluster - config.AuthInfos["default"] = authInfo - config.Contexts["default"] = context - config.CurrentContext = "default" - - kubeconfig, err := clientcmd.Write(*config) - if err != nil { - return nil, err - } - - return kubeconfig, nil -} - func basicAuth(username, password string) string { auth := username + ":" + password return base64.StdEncoding.EncodeToString([]byte(auth)) diff --git a/pkg/controller/cluster/server/ingress.go b/pkg/controller/cluster/server/ingress.go index c0769348..a42a1f47 100644 --- a/pkg/controller/cluster/server/ingress.go +++ b/pkg/controller/cluster/server/ingress.go @@ -15,6 +15,8 @@ const ( nginxSSLPassthroughAnnotation = "nginx.ingress.kubernetes.io/ssl-passthrough" nginxBackendProtocolAnnotation = "nginx.ingress.kubernetes.io/backend-protocol" nginxSSLRedirectAnnotation = "nginx.ingress.kubernetes.io/ssl-redirect" + serverPort = 6443 + etcdPort = 2379 ) func (s *Server) Ingress(ctx context.Context, client client.Client) (*networkingv1.Ingress, error) { @@ -59,7 +61,7 @@ func (s *Server) ingressRules(addresses []string) []networkingv1.IngressRule { Service: &networkingv1.IngressServiceBackend{ Name: "k3k-server-service", Port: networkingv1.ServiceBackendPort{ - Number: port, + Number: serverPort, }, }, }, diff --git a/pkg/controller/cluster/server/service.go b/pkg/controller/cluster/server/service.go index 05922b23..fb99bcf3 100644 --- a/pkg/controller/cluster/server/service.go +++ b/pkg/controller/cluster/server/service.go @@ -36,12 +36,12 @@ func (s *Server) Service(cluster *v1alpha1.Cluster) *v1.Service { { Name: "k3s-server-port", Protocol: v1.ProtocolTCP, - Port: port, + Port: serverPort, }, { Name: "k3s-etcd-port", Protocol: v1.ProtocolTCP, - Port: 2379, + Port: etcdPort, }, }, }, @@ -70,12 +70,12 @@ func (s *Server) StatefulServerService(cluster *v1alpha1.Cluster) *v1.Service { { Name: "k3s-server-port", Protocol: v1.ProtocolTCP, - Port: 6443, + Port: serverPort, }, { Name: "k3s-etcd-port", Protocol: v1.ProtocolTCP, - Port: 2379, + Port: etcdPort, }, }, }, diff --git a/pkg/controller/kubeconfig/certs.go b/pkg/controller/kubeconfig/certs.go new file mode 100644 index 00000000..276e4558 --- /dev/null +++ b/pkg/controller/kubeconfig/certs.go @@ -0,0 +1,71 @@ +package kubeconfig + +import ( + "crypto" + "crypto/x509" + "fmt" + "net" + "time" + + certutil "github.com/rancher/dynamiclistener/cert" +) + +func CreateClientCertKey(commonName string, organization []string, altNames *certutil.AltNames, extKeyUsage []x509.ExtKeyUsage, expiresAt time.Duration, caCert, caKey string) ([]byte, []byte, error) { + caKeyPEM, err := certutil.ParsePrivateKeyPEM([]byte(caKey)) + if err != nil { + return nil, nil, err + } + + caCertPEM, err := certutil.ParseCertsPEM([]byte(caCert)) + if err != nil { + return nil, nil, err + } + + b, err := generateKey() + if err != nil { + return nil, nil, err + } + + key, err := certutil.ParsePrivateKeyPEM(b) + if err != nil { + return nil, nil, err + } + + cfg := certutil.Config{ + CommonName: commonName, + Organization: organization, + Usages: extKeyUsage, + ExpiresAt: expiresAt, + } + if altNames != nil { + cfg.AltNames = *altNames + } + cert, err := certutil.NewSignedCert(cfg, key.(crypto.Signer), caCertPEM[0], caKeyPEM.(crypto.Signer)) + if err != nil { + return nil, nil, err + } + + return append(certutil.EncodeCertPEM(cert), certutil.EncodeCertPEM(caCertPEM[0])...), b, nil +} + +func generateKey() (data []byte, err error) { + generatedData, err := certutil.MakeEllipticPrivateKeyPEM() + if err != nil { + return nil, fmt.Errorf("error generating key: %v", err) + } + + return generatedData, nil +} + +func AddSANs(sans []string) certutil.AltNames { + var altNames certutil.AltNames + for _, san := range sans { + ip := net.ParseIP(san) + if ip == nil { + altNames.DNSNames = append(altNames.DNSNames, san) + } else { + altNames.IPs = append(altNames.IPs, ip) + } + } + return altNames +} diff --git a/pkg/controller/kubeconfig/kubeconfig.go b/pkg/controller/kubeconfig/kubeconfig.go new file mode 100644 index 00000000..dbccf6e5 --- /dev/null +++ b/pkg/controller/kubeconfig/kubeconfig.go @@ -0,0 +1,108 @@ +package kubeconfig + +import ( + "context" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "time" + + certutil "github.com/rancher/dynamiclistener/cert" + "github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1" + "github.com/rancher/k3k/pkg/controller/cluster/server/bootstrap" + "github.com/rancher/k3k/pkg/controller/util" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type KubeConfig struct { + AltNames certutil.AltNames + CN string + ORG []string + ExpiryDate time.Duration +} + +func (k *KubeConfig) Extract(ctx context.Context, client client.Client, cluster *v1alpha1.Cluster, hostServerIP string) ([]byte, error) { + nn := types.NamespacedName{ + Name: cluster.Name + "-bootstrap", + Namespace: util.ClusterNamespace(cluster), + } + + var bootstrapSecret v1.Secret + if err := client.Get(ctx, nn, &bootstrapSecret); err != nil { + return nil, err + } + + bootstrapData := bootstrapSecret.Data["bootstrap"] + if bootstrapData == nil { + return nil, errors.New("empty bootstrap") + } + + var bootstrap bootstrap.ControlRuntimeBootstrap + if err := json.Unmarshal(bootstrapData, &bootstrap); err != nil { + return nil, err + } + + adminCert, adminKey, err := CreateClientCertKey( + k.CN, k.ORG, + &k.AltNames, []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, k.ExpiryDate, + bootstrap.ClientCA.Content, + bootstrap.ClientCAKey.Content) + if err != nil { + return nil, err + } + // get the server service to extract the right IP + nn = types.NamespacedName{ + Name: "k3k-server-service", + Namespace: util.ClusterNamespace(cluster), + } + + var k3kService v1.Service + if err := client.Get(ctx, nn, &k3kService); err != nil { + return nil, err + } + + url := fmt.Sprintf("https://%s:%d", k3kService.Spec.ClusterIP, util.ServerPort) + if k3kService.Spec.Type == v1.ServiceTypeNodePort { + nodePort := k3kService.Spec.Ports[0].NodePort + url = fmt.Sprintf("https://%s:%d", hostServerIP, nodePort) + } + kubeconfigData, err := kubeconfig(url, []byte(bootstrap.ServerCA.Content), adminCert, adminKey) + if err != nil { + return nil, err + } + + return kubeconfigData, nil +} + +func kubeconfig(url string, serverCA, clientCert, clientKey []byte) ([]byte, error) { + config := clientcmdapi.NewConfig() + + cluster := clientcmdapi.NewCluster() + cluster.CertificateAuthorityData = serverCA + cluster.Server = url + + authInfo := clientcmdapi.NewAuthInfo() + authInfo.ClientCertificateData = clientCert + authInfo.ClientKeyData = clientKey + + context := clientcmdapi.NewContext() + context.AuthInfo = "default" + context.Cluster = "default" + + config.Clusters["default"] = cluster + config.AuthInfos["default"] = authInfo + config.Contexts["default"] = context + config.CurrentContext = "default" + + kubeconfig, err := clientcmd.Write(*config) + if err != nil { + return nil, err + } + + return kubeconfig, nil +} diff --git a/pkg/controller/util/util.go b/pkg/controller/util/util.go index e9ebece3..7b2f55b8 100644 --- a/pkg/controller/util/util.go +++ b/pkg/controller/util/util.go @@ -12,6 +12,8 @@ import ( const ( namespacePrefix = "k3k-" k3SImageName = "rancher/k3s" + AdminCommonName = "system:admin" + ServerPort = 6443 ) const (