From d909a51d566c754e155d188f4244c77255f4c983 Mon Sep 17 00:00:00 2001 From: Enrico Candino Date: Tue, 4 Feb 2025 16:20:52 +0100 Subject: [PATCH] cli refactor --- cli/cmds/cluster/cluster.go | 25 +-- cli/cmds/cluster/create.go | 286 ------------------------------- cli/cmds/cluster/create_cmd.go | 202 ++++++++++++++++++++++ cli/cmds/cluster/create_flags.go | 99 +++++++++++ cli/cmds/cluster/delete.go | 25 ++- cli/cmds/root.go | 7 +- 6 files changed, 329 insertions(+), 315 deletions(-) delete mode 100644 cli/cmds/cluster/create.go create mode 100644 cli/cmds/cluster/create_cmd.go create mode 100644 cli/cmds/cluster/create_flags.go diff --git a/cli/cmds/cluster/cluster.go b/cli/cmds/cluster/cluster.go index 5339c7e..d39c5a6 100644 --- a/cli/cmds/cluster/cluster.go +++ b/cli/cmds/cluster/cluster.go @@ -1,29 +1,16 @@ package cluster import ( - "github.com/rancher/k3k/cli/cmds" "github.com/urfave/cli/v2" ) -var subcommands = []*cli.Command{ - { - Name: "create", - Usage: "Create new cluster", - Action: create, - Flags: append(cmds.CommonFlags, clusterCreateFlags...), - }, - { - Name: "delete", - Usage: "Delete an existing cluster", - Action: delete, - Flags: append(cmds.CommonFlags, clusterDeleteFlags...), - }, -} - func NewCommand() *cli.Command { return &cli.Command{ - Name: "cluster", - Usage: "cluster command", - Subcommands: subcommands, + Name: "cluster", + Usage: "cluster command", + Subcommands: []*cli.Command{ + NewCreateCmd(), + NewDeleteCmd(), + }, } } diff --git a/cli/cmds/cluster/create.go b/cli/cmds/cluster/create.go deleted file mode 100644 index 8ae57df..0000000 --- a/cli/cmds/cluster/create.go +++ /dev/null @@ -1,286 +0,0 @@ -package cluster - -import ( - "context" - "errors" - "net/url" - "os" - "path/filepath" - "strings" - "time" - - "github.com/rancher/k3k/cli/cmds" - "github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1" - k3kcluster "github.com/rancher/k3k/pkg/controller/cluster" - "github.com/rancher/k3k/pkg/controller/cluster/server" - "github.com/rancher/k3k/pkg/controller/kubeconfig" - "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" - 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/util/wait" - clientgoscheme "k8s.io/client-go/kubernetes/scheme" - "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" -) - -var Scheme = runtime.NewScheme() - -func init() { - _ = clientgoscheme.AddToScheme(Scheme) - _ = v1alpha1.AddToScheme(Scheme) -} - -var ( - name string - token string - clusterCIDR string - serviceCIDR string - servers int64 - agents int64 - serverArgs cli.StringSlice - agentArgs cli.StringSlice - persistenceType string - storageClassName string - version string - mode string - kubeconfigServerHost string - - clusterCreateFlags = []cli.Flag{ - &cli.StringFlag{ - Name: "name", - Usage: "name of the cluster", - Destination: &name, - }, - &cli.Int64Flag{ - Name: "servers", - Usage: "number of servers", - Destination: &servers, - Value: 1, - }, - &cli.Int64Flag{ - Name: "agents", - Usage: "number of agents", - Destination: &agents, - }, - &cli.StringFlag{ - Name: "token", - Usage: "token of the cluster", - Destination: &token, - }, - &cli.StringFlag{ - Name: "cluster-cidr", - Usage: "cluster CIDR", - Destination: &clusterCIDR, - }, - &cli.StringFlag{ - Name: "service-cidr", - Usage: "service CIDR", - Destination: &serviceCIDR, - }, - &cli.StringFlag{ - Name: "persistence-type", - Usage: "Persistence mode for the nodes (ephemeral, static, dynamic)", - Value: server.EphemeralNodesType, - Destination: &persistenceType, - }, - &cli.StringFlag{ - Name: "storage-class-name", - Usage: "Storage class name for dynamic persistence type", - Destination: &storageClassName, - }, - &cli.StringSliceFlag{ - Name: "server-args", - Usage: "servers extra arguments", - Value: &serverArgs, - }, - &cli.StringSliceFlag{ - Name: "agent-args", - Usage: "agents extra arguments", - Value: &agentArgs, - }, - &cli.StringFlag{ - Name: "version", - Usage: "k3s version", - Destination: &version, - }, - &cli.StringFlag{ - Name: "mode", - Usage: "k3k mode type", - Destination: &mode, - Value: "shared", - }, - &cli.StringFlag{ - Name: "kubeconfig-server", - Usage: "override the kubeconfig server host", - Destination: &kubeconfigServerHost, - Value: "", - }, - } -) - -func create(clx *cli.Context) error { - ctx := context.Background() - if err := validateCreateFlags(); err != nil { - return err - } - - 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 - } - if token != "" { - logrus.Infof("Creating cluster token secret") - obj := k3kcluster.TokenSecretObj(token, name, cmds.Namespace()) - if err := ctrlClient.Create(ctx, &obj); err != nil { - return err - } - } - logrus.Infof("Creating a new cluster [%s]", name) - cluster := newCluster( - name, - cmds.Namespace(), - mode, - token, - int32(servers), - int32(agents), - clusterCIDR, - serviceCIDR, - serverArgs.Value(), - agentArgs.Value(), - ) - - cluster.Spec.Expose = &v1alpha1.ExposeConfig{ - NodePort: &v1alpha1.NodePortConfig{ - Enabled: true, - }, - } - - // add Host IP address as an extra TLS-SAN to expose the k3k cluster - url, err := url.Parse(restConfig.Host) - if err != nil { - return err - } - host := strings.Split(url.Host, ":") - if kubeconfigServerHost != "" { - host = []string{kubeconfigServerHost} - } - cluster.Spec.TLSSANs = []string{host[0]} - - if err := ctrlClient.Create(ctx, cluster); err != nil { - if apierrors.IsAlreadyExists(err) { - logrus.Infof("Cluster [%s] already exists", name) - } else { - return err - } - } - - logrus.Infof("Extracting Kubeconfig for [%s] cluster", name) - - logrus.Infof("waiting for cluster to be available..") - - // retry every 5s for at most 2m, or 25 times - availableBackoff := wait.Backoff{ - Duration: 5 * time.Second, - Cap: 2 * time.Minute, - Steps: 25, - } - - cfg := kubeconfig.New() - - var kubeconfig *clientcmdapi.Config - if err := retry.OnError(availableBackoff, apierrors.IsNotFound, func() error { - kubeconfig, err = cfg.Extract(ctx, ctrlClient, cluster, host[0]) - return err - }); err != nil { - return err - } - - pwd, err := os.Getwd() - if err != nil { - return err - } - - logrus.Infof(`You can start using the cluster with: - - export KUBECONFIG=%s - kubectl cluster-info - `, filepath.Join(pwd, cluster.Name+"-kubeconfig.yaml")) - - kubeconfigData, err := clientcmd.Write(*kubeconfig) - if err != nil { - return err - } - - return os.WriteFile(cluster.Name+"-kubeconfig.yaml", kubeconfigData, 0644) -} - -func validateCreateFlags() error { - if persistenceType != server.EphemeralNodesType && - persistenceType != server.DynamicNodesType { - return errors.New("invalid persistence type") - } - if name == "" { - return errors.New("empty cluster name") - } - if name == k3kcluster.ClusterInvalidName { - return errors.New("invalid cluster name") - } - if servers <= 0 { - return errors.New("invalid number of servers") - } - if cmds.Kubeconfig == "" && os.Getenv("KUBECONFIG") == "" { - return errors.New("empty kubeconfig") - } - if mode != "shared" && mode != "virtual" { - return errors.New(`mode should be one of "shared" or "virtual"`) - } - - return nil -} - -func newCluster(name, namespace, mode, token string, servers, agents int32, clusterCIDR, serviceCIDR string, serverArgs, agentArgs []string) *v1alpha1.Cluster { - cluster := &v1alpha1.Cluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - TypeMeta: metav1.TypeMeta{ - Kind: "Cluster", - APIVersion: "k3k.io/v1alpha1", - }, - Spec: v1alpha1.ClusterSpec{ - Servers: &servers, - Agents: &agents, - ClusterCIDR: clusterCIDR, - ServiceCIDR: serviceCIDR, - ServerArgs: serverArgs, - AgentArgs: agentArgs, - Version: version, - Mode: v1alpha1.ClusterMode(mode), - Persistence: &v1alpha1.PersistenceConfig{ - Type: persistenceType, - StorageClassName: storageClassName, - }, - }, - } - if token != "" { - cluster.Spec.TokenSecretRef = &v1.SecretReference{ - Name: k3kcluster.TokenSecretName(name), - Namespace: namespace, - } - } - return cluster -} diff --git a/cli/cmds/cluster/create_cmd.go b/cli/cmds/cluster/create_cmd.go new file mode 100644 index 0000000..8795705 --- /dev/null +++ b/cli/cmds/cluster/create_cmd.go @@ -0,0 +1,202 @@ +package cluster + +import ( + "context" + "errors" + "net/url" + "os" + "path/filepath" + "strings" + "time" + + "github.com/rancher/k3k/cli/cmds" + "github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1" + k3kcluster "github.com/rancher/k3k/pkg/controller/cluster" + "github.com/rancher/k3k/pkg/controller/kubeconfig" + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" + 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/util/wait" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/client-go/util/retry" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var Scheme = runtime.NewScheme() + +func init() { + _ = clientgoscheme.AddToScheme(Scheme) + _ = v1alpha1.AddToScheme(Scheme) +} + +type CreateConfig struct { + token string + clusterCIDR string + serviceCIDR string + servers int + agents int + serverArgs cli.StringSlice + agentArgs cli.StringSlice + persistenceType string + storageClassName string + version string + mode string + kubeconfigServerHost string +} + +func NewCreateCmd() *cli.Command { + createConfig := &CreateConfig{} + createFlags := NewCreateFlags(createConfig) + + return &cli.Command{ + Name: "create", + Usage: "Create new cluster", + Action: createAction(createConfig), + Flags: append(cmds.CommonFlags, createFlags...), + Args: false, + ArgsUsage: "NAME", + } +} + +func createAction(config *CreateConfig) cli.ActionFunc { + return func(clx *cli.Context) error { + ctx := context.Background() + + name := clx.Args().First() + if name == "" { + return errors.New("empty cluster name") + } else if name == k3kcluster.ClusterInvalidName { + return errors.New("invalid cluster name") + } + + 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 + } + + if config.token != "" { + logrus.Infof("Creating cluster token secret") + obj := k3kcluster.TokenSecretObj(config.token, name, cmds.Namespace()) + if err := ctrlClient.Create(ctx, &obj); err != nil { + return err + } + } + + logrus.Infof("Creating a new cluster [%s]", name) + + cluster := newCluster(name, cmds.Namespace(), config) + + cluster.Spec.Expose = &v1alpha1.ExposeConfig{ + NodePort: &v1alpha1.NodePortConfig{ + Enabled: true, + }, + } + + // add Host IP address as an extra TLS-SAN to expose the k3k cluster + url, err := url.Parse(restConfig.Host) + if err != nil { + return err + } + host := strings.Split(url.Host, ":") + if config.kubeconfigServerHost != "" { + host = []string{config.kubeconfigServerHost} + } + cluster.Spec.TLSSANs = []string{host[0]} + + if err := ctrlClient.Create(ctx, cluster); err != nil { + if apierrors.IsAlreadyExists(err) { + logrus.Infof("Cluster [%s] already exists", name) + } else { + return err + } + } + + logrus.Infof("Extracting Kubeconfig for [%s] cluster", name) + + logrus.Infof("waiting for cluster to be available..") + + // retry every 5s for at most 2m, or 25 times + availableBackoff := wait.Backoff{ + Duration: 5 * time.Second, + Cap: 2 * time.Minute, + Steps: 25, + } + + cfg := kubeconfig.New() + + var kubeconfig *clientcmdapi.Config + if err := retry.OnError(availableBackoff, apierrors.IsNotFound, func() error { + kubeconfig, err = cfg.Extract(ctx, ctrlClient, cluster, host[0]) + return err + }); err != nil { + return err + } + + pwd, err := os.Getwd() + if err != nil { + return err + } + + logrus.Infof(`You can start using the cluster with: + + export KUBECONFIG=%s + kubectl cluster-info + `, filepath.Join(pwd, cluster.Name+"-kubeconfig.yaml")) + + kubeconfigData, err := clientcmd.Write(*kubeconfig) + if err != nil { + return err + } + + return os.WriteFile(cluster.Name+"-kubeconfig.yaml", kubeconfigData, 0644) + } +} + +func newCluster(name, namespace string, config *CreateConfig) *v1alpha1.Cluster { + cluster := &v1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + TypeMeta: metav1.TypeMeta{ + Kind: "Cluster", + APIVersion: "k3k.io/v1alpha1", + }, + Spec: v1alpha1.ClusterSpec{ + Servers: ptr.To(int32(config.servers)), + Agents: ptr.To(int32(config.agents)), + ClusterCIDR: config.clusterCIDR, + ServiceCIDR: config.serviceCIDR, + ServerArgs: config.serverArgs.Value(), + AgentArgs: config.agentArgs.Value(), + Version: config.version, + Mode: v1alpha1.ClusterMode(config.mode), + Persistence: &v1alpha1.PersistenceConfig{ + Type: config.persistenceType, + StorageClassName: config.storageClassName, + }, + }, + } + + if config.token != "" { + cluster.Spec.TokenSecretRef = &v1.SecretReference{ + Name: k3kcluster.TokenSecretName(name), + Namespace: namespace, + } + } + + return cluster +} diff --git a/cli/cmds/cluster/create_flags.go b/cli/cmds/cluster/create_flags.go new file mode 100644 index 0000000..abbc6f0 --- /dev/null +++ b/cli/cmds/cluster/create_flags.go @@ -0,0 +1,99 @@ +package cluster + +import ( + "errors" + + "github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1" + "github.com/rancher/k3k/pkg/controller/cluster/server" + "github.com/urfave/cli/v2" +) + +func NewCreateFlags(config *CreateConfig) []cli.Flag { + return []cli.Flag{ + &cli.IntFlag{ + Name: "servers", + Usage: "number of servers", + Destination: &config.servers, + Value: 1, + Action: func(ctx *cli.Context, value int) error { + if value <= 0 { + return errors.New("invalid number of servers") + } + return nil + }, + }, + &cli.IntFlag{ + Name: "agents", + Usage: "number of agents", + Destination: &config.agents, + }, + &cli.StringFlag{ + Name: "token", + Usage: "token of the cluster", + Destination: &config.token, + }, + &cli.StringFlag{ + Name: "cluster-cidr", + Usage: "cluster CIDR", + Destination: &config.clusterCIDR, + }, + &cli.StringFlag{ + Name: "service-cidr", + Usage: "service CIDR", + Destination: &config.serviceCIDR, + }, + &cli.StringFlag{ + Name: "persistence-type", + Usage: "persistence mode for the nodes (ephemeral, static, dynamic)", + Value: server.EphemeralNodesType, + Destination: &config.persistenceType, + Action: func(ctx *cli.Context, value string) error { + switch value { + case server.EphemeralNodesType, server.DynamicNodesType: + return nil + default: + return errors.New(`persistence-type should be one of "ephemeral", "static" or "dynamic"`) + } + }, + }, + &cli.StringFlag{ + Name: "storage-class-name", + Usage: "storage class name for dynamic persistence type", + Destination: &config.storageClassName, + }, + &cli.StringSliceFlag{ + Name: "server-args", + Usage: "servers extra arguments", + Value: &config.serverArgs, + }, + &cli.StringSliceFlag{ + Name: "agent-args", + Usage: "agents extra arguments", + Value: &config.agentArgs, + }, + &cli.StringFlag{ + Name: "version", + Usage: "k3s version", + Destination: &config.version, + }, + &cli.StringFlag{ + Name: "mode", + Usage: "k3k mode type", + Destination: &config.mode, + Value: "shared", + Action: func(ctx *cli.Context, value string) error { + switch value { + case string(v1alpha1.VirtualClusterMode), string(v1alpha1.SharedClusterMode): + return nil + default: + return errors.New(`mode should be one of "shared" or "virtual"`) + } + }, + }, + &cli.StringFlag{ + Name: "kubeconfig-server", + Usage: "override the kubeconfig server host", + Destination: &config.kubeconfigServerHost, + }, + } +} diff --git a/cli/cmds/cluster/delete.go b/cli/cmds/cluster/delete.go index 716f6a4..ad90d90 100644 --- a/cli/cmds/cluster/delete.go +++ b/cli/cmds/cluster/delete.go @@ -2,9 +2,11 @@ package cluster import ( "context" + "errors" "github.com/rancher/k3k/cli/cmds" "github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1" + k3kcluster "github.com/rancher/k3k/pkg/controller/cluster" "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -12,19 +14,25 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -var ( - clusterDeleteFlags = []cli.Flag{ - &cli.StringFlag{ - Name: "name", - Usage: "name of the cluster", - Destination: &name, - }, +func NewDeleteCmd() *cli.Command { + return &cli.Command{ + Name: "delete", + Usage: "Delete an existing cluster", + Action: delete, + Flags: cmds.CommonFlags, } -) +} func delete(clx *cli.Context) error { ctx := context.Background() + name := clx.Args().First() + if name == "" { + return errors.New("empty cluster name") + } else if name == k3kcluster.ClusterInvalidName { + return errors.New("invalid cluster name") + } + restConfig, err := clientcmd.BuildConfigFromFlags("", cmds.Kubeconfig) if err != nil { return err @@ -38,6 +46,7 @@ func delete(clx *cli.Context) error { } logrus.Infof("deleting [%s] cluster", name) + cluster := v1alpha1.Cluster{ ObjectMeta: metav1.ObjectMeta{ Name: name, diff --git a/cli/cmds/root.go b/cli/cmds/root.go index 203348a..ab36b2c 100644 --- a/cli/cmds/root.go +++ b/cli/cmds/root.go @@ -1,6 +1,8 @@ package cmds import ( + "os" + "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) @@ -17,12 +19,13 @@ var ( &cli.StringFlag{ Name: "kubeconfig", EnvVars: []string{"KUBECONFIG"}, - Usage: "Kubeconfig path", + Usage: "kubeconfig path", Destination: &Kubeconfig, + Value: os.Getenv("HOME") + "/.kube/config", }, &cli.StringFlag{ Name: "namespace", - Usage: "Namespace to create the k3k cluster in", + Usage: "namespace to create the k3k cluster in", Destination: &namespace, }, }