From 5758b880a518be93bba2edabceefd8e2a33c72e0 Mon Sep 17 00:00:00 2001 From: Enrico Candino Date: Fri, 30 May 2025 15:35:31 +0200 Subject: [PATCH] Added `k3kcli cluster list` and `k3kcli policy list` commands (#368) * list commands * go mod tidy * moved logic to separate file, small refactor --- charts/k3k/crds/k3k.io_clusters.yaml | 6 +- .../crds/k3k.io_virtualclusterpolicies.yaml | 3 - cli/cmds/cluster.go | 1 + cli/cmds/cluster_list.go | 51 +++++++++ cli/cmds/policy.go | 1 + cli/cmds/policy_list.go | 50 +++++++++ cli/cmds/root.go | 3 + cli/cmds/table_printer.go | 103 ++++++++++++++++++ docs/cli/cli-docs.md | 38 +++++-- go.mod | 4 +- pkg/apis/k3k.io/v1alpha1/types.go | 2 +- 11 files changed, 248 insertions(+), 14 deletions(-) create mode 100644 cli/cmds/cluster_list.go create mode 100644 cli/cmds/policy_list.go create mode 100644 cli/cmds/table_printer.go diff --git a/charts/k3k/crds/k3k.io_clusters.yaml b/charts/k3k/crds/k3k.io_clusters.yaml index 9e4acd62..85bf5929 100644 --- a/charts/k3k/crds/k3k.io_clusters.yaml +++ b/charts/k3k/crds/k3k.io_clusters.yaml @@ -14,7 +14,11 @@ spec: singular: cluster scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - jsonPath: .spec.mode + name: Mode + type: string + name: v1alpha1 schema: openAPIV3Schema: description: |- diff --git a/charts/k3k/crds/k3k.io_virtualclusterpolicies.yaml b/charts/k3k/crds/k3k.io_virtualclusterpolicies.yaml index 9d050f8d..e00a3966 100644 --- a/charts/k3k/crds/k3k.io_virtualclusterpolicies.yaml +++ b/charts/k3k/crds/k3k.io_virtualclusterpolicies.yaml @@ -20,9 +20,6 @@ spec: - jsonPath: .spec.allowedMode name: Mode type: string - - jsonPath: .metadata.creationTimestamp - name: Age - type: date name: v1alpha1 schema: openAPIV3Schema: diff --git a/cli/cmds/cluster.go b/cli/cmds/cluster.go index 9ca2129e..d7363e2e 100644 --- a/cli/cmds/cluster.go +++ b/cli/cmds/cluster.go @@ -11,6 +11,7 @@ func NewClusterCmd(appCtx *AppContext) *cli.Command { Subcommands: []*cli.Command{ NewClusterCreateCmd(appCtx), NewClusterDeleteCmd(appCtx), + NewClusterListCmd(appCtx), }, } } diff --git a/cli/cmds/cluster_list.go b/cli/cmds/cluster_list.go new file mode 100644 index 00000000..bbc45058 --- /dev/null +++ b/cli/cmds/cluster_list.go @@ -0,0 +1,51 @@ +package cmds + +import ( + "context" + + "github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1" + "github.com/urfave/cli/v2" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/cli-runtime/pkg/printers" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +func NewClusterListCmd(appCtx *AppContext) *cli.Command { + return &cli.Command{ + Name: "list", + Usage: "List all the existing cluster", + UsageText: "k3kcli cluster list [command options]", + Action: list(appCtx), + Flags: WithCommonFlags(appCtx), + HideHelpCommand: true, + } +} + +func list(appCtx *AppContext) cli.ActionFunc { + return func(clx *cli.Context) error { + ctx := context.Background() + client := appCtx.Client + + if clx.NArg() > 0 { + return cli.ShowSubcommandHelp(clx) + } + + var clusters v1alpha1.ClusterList + if err := client.List(ctx, &clusters, ctrlclient.InNamespace(appCtx.namespace)); err != nil { + return err + } + + crd := &apiextensionsv1.CustomResourceDefinition{} + if err := client.Get(ctx, types.NamespacedName{Name: "clusters.k3k.io"}, crd); err != nil { + return err + } + + items := toPointerSlice(clusters.Items) + table := createTable(crd, items) + + printer := printers.NewTablePrinter(printers.PrintOptions{WithNamespace: true}) + + return printer.PrintObj(table, clx.App.Writer) + } +} diff --git a/cli/cmds/policy.go b/cli/cmds/policy.go index d8231f8e..342384c4 100644 --- a/cli/cmds/policy.go +++ b/cli/cmds/policy.go @@ -11,6 +11,7 @@ func NewPolicyCmd(appCtx *AppContext) *cli.Command { Subcommands: []*cli.Command{ NewPolicyCreateCmd(appCtx), NewPolicyDeleteCmd(appCtx), + NewPolicyListCmd(appCtx), }, } } diff --git a/cli/cmds/policy_list.go b/cli/cmds/policy_list.go new file mode 100644 index 00000000..6b8d5a44 --- /dev/null +++ b/cli/cmds/policy_list.go @@ -0,0 +1,50 @@ +package cmds + +import ( + "context" + + "github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1" + "github.com/urfave/cli/v2" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/cli-runtime/pkg/printers" +) + +func NewPolicyListCmd(appCtx *AppContext) *cli.Command { + return &cli.Command{ + Name: "list", + Usage: "List all the existing policies", + UsageText: "k3kcli policy list [command options]", + Action: policyList(appCtx), + Flags: WithCommonFlags(appCtx), + HideHelpCommand: true, + } +} + +func policyList(appCtx *AppContext) cli.ActionFunc { + return func(clx *cli.Context) error { + ctx := context.Background() + client := appCtx.Client + + if clx.NArg() > 0 { + return cli.ShowSubcommandHelp(clx) + } + + var policies v1alpha1.VirtualClusterPolicyList + if err := client.List(ctx, &policies); err != nil { + return err + } + + crd := &apiextensionsv1.CustomResourceDefinition{} + if err := client.Get(ctx, types.NamespacedName{Name: "virtualclusterpolicies.k3k.io"}, crd); err != nil { + return err + } + + items := toPointerSlice(policies.Items) + table := createTable(crd, items) + + printer := printers.NewTablePrinter(printers.PrintOptions{}) + + return printer.PrintObj(table, clx.App.Writer) + } +} diff --git a/cli/cmds/root.go b/cli/cmds/root.go index e8e1c649..7464ec50 100644 --- a/cli/cmds/root.go +++ b/cli/cmds/root.go @@ -7,6 +7,7 @@ import ( "github.com/rancher/k3k/pkg/buildinfo" "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" @@ -45,6 +46,7 @@ func NewApp() *cli.App { scheme := runtime.NewScheme() _ = clientgoscheme.AddToScheme(scheme) _ = v1alpha1.AddToScheme(scheme) + _ = apiextensionsv1.AddToScheme(scheme) ctrlClient, err := client.New(restConfig, client.Options{Scheme: scheme}) if err != nil { @@ -109,6 +111,7 @@ func WithCommonFlags(appCtx *AppContext, flags ...cli.Flag) []cli.Flag { &cli.StringFlag{ Name: "namespace", Usage: "namespace to create the k3k cluster in", + Aliases: []string{"n"}, Destination: &appCtx.namespace, }, } diff --git a/cli/cmds/table_printer.go b/cli/cmds/table_printer.go new file mode 100644 index 00000000..53f45656 --- /dev/null +++ b/cli/cmds/table_printer.go @@ -0,0 +1,103 @@ +package cmds + +import ( + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/util/jsonpath" +) + +// createTable creates a table to print from the printerColumn defined in the CRD spec, plus the name at the beginning +func createTable[T runtime.Object](crd *apiextensionsv1.CustomResourceDefinition, objs []T) *metav1.Table { + printerColumns := getPrinterColumnsFromCRD(crd) + + return &metav1.Table{ + TypeMeta: metav1.TypeMeta{APIVersion: "meta.k8s.io/v1", Kind: "Table"}, + ColumnDefinitions: convertToTableColumns(printerColumns), + Rows: createTableRows(objs, printerColumns), + } +} + +func getPrinterColumnsFromCRD(crd *apiextensionsv1.CustomResourceDefinition) []apiextensionsv1.CustomResourceColumnDefinition { + printerColumns := []apiextensionsv1.CustomResourceColumnDefinition{ + {Name: "Name", Type: "string", Format: "name", Description: "Name of the Resource", JSONPath: ".metadata.name"}, + } + + for _, version := range crd.Spec.Versions { + if version.Name == "v1alpha1" { + printerColumns = append(printerColumns, version.AdditionalPrinterColumns...) + break + } + } + + return printerColumns +} + +func convertToTableColumns(printerColumns []apiextensionsv1.CustomResourceColumnDefinition) []metav1.TableColumnDefinition { + var columnDefinitions []metav1.TableColumnDefinition + + for _, col := range printerColumns { + columnDefinitions = append(columnDefinitions, metav1.TableColumnDefinition{ + Name: col.Name, + Type: col.Type, + Format: col.Format, + Description: col.Description, + Priority: col.Priority, + }) + } + + return columnDefinitions +} + +func createTableRows[T runtime.Object](objs []T, printerColumns []apiextensionsv1.CustomResourceColumnDefinition) []metav1.TableRow { + var rows []metav1.TableRow + + for _, obj := range objs { + objMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&obj) + if err != nil { + rows = append(rows, metav1.TableRow{Cells: []any{""}}) + continue + } + + rows = append(rows, metav1.TableRow{ + Cells: buildRowCells(objMap, printerColumns), + Object: runtime.RawExtension{Object: obj}, + }) + } + + return rows +} + +func buildRowCells(objMap map[string]any, printerColumns []apiextensionsv1.CustomResourceColumnDefinition) []any { + var cells []any + + for _, printCol := range printerColumns { + j := jsonpath.New(printCol.Name) + + err := j.Parse("{" + printCol.JSONPath + "}") + if err != nil { + cells = append(cells, "") + continue + } + + results, err := j.FindResults(objMap) + if err != nil || len(results) == 0 || len(results[0]) == 0 { + cells = append(cells, "") + continue + } + + cells = append(cells, results[0][0].Interface()) + } + + return cells +} + +func toPointerSlice[T any](v []T) []*T { + var vPtr = make([]*T, len(v)) + + for i := range v { + vPtr[i] = &v[i] + } + + return vPtr +} diff --git a/docs/cli/cli-docs.md b/docs/cli/cli-docs.md index 63f97d88..c7b4cef4 100644 --- a/docs/cli/cli-docs.md +++ b/docs/cli/cli-docs.md @@ -9,7 +9,7 @@ k3kcli ``` [--debug] [--kubeconfig]=[value] -[--namespace]=[value] +[--namespace|-n]=[value] ``` **Usage**: @@ -24,7 +24,7 @@ k3kcli [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] **--kubeconfig**="": kubeconfig path (default: $HOME/.kube/config or $KUBECONFIG if set) -**--namespace**="": namespace to create the k3k cluster in +**--namespace, -n**="": namespace to create the k3k cluster in # COMMANDS @@ -55,7 +55,7 @@ Create new cluster **--mode**="": k3k mode type (shared, virtual) (default: "shared") -**--namespace**="": namespace to create the k3k cluster in +**--namespace, -n**="": namespace to create the k3k cluster in **--persistence-type**="": persistence mode for the nodes (dynamic, ephemeral, static) (default: "dynamic") @@ -87,7 +87,19 @@ Delete an existing cluster **--kubeconfig**="": kubeconfig path (default: $HOME/.kube/config or $KUBECONFIG if set) -**--namespace**="": namespace to create the k3k cluster in +**--namespace, -n**="": namespace to create the k3k cluster in + +### list + +List all the existing cluster + +>k3kcli cluster list [command options] + +**--debug**: Turn on debug logs + +**--kubeconfig**="": kubeconfig path (default: $HOME/.kube/config or $KUBECONFIG if set) + +**--namespace, -n**="": namespace to create the k3k cluster in ## policy @@ -105,7 +117,7 @@ Create new policy **--mode**="": The allowed mode type of the policy (default: "shared") -**--namespace**="": namespace to create the k3k cluster in +**--namespace, -n**="": namespace to create the k3k cluster in ### delete @@ -117,7 +129,19 @@ Delete an existing policy **--kubeconfig**="": kubeconfig path (default: $HOME/.kube/config or $KUBECONFIG if set) -**--namespace**="": namespace to create the k3k cluster in +**--namespace, -n**="": namespace to create the k3k cluster in + +### list + +List all the existing policies + +>k3kcli policy list [command options] + +**--debug**: Turn on debug logs + +**--kubeconfig**="": kubeconfig path (default: $HOME/.kube/config or $KUBECONFIG if set) + +**--namespace, -n**="": namespace to create the k3k cluster in ## kubeconfig @@ -143,6 +167,6 @@ Generate kubeconfig for clusters **--name**="": cluster name -**--namespace**="": namespace to create the k3k cluster in +**--namespace, -n**="": namespace to create the k3k cluster in **--org**="": Organization name (ORG) of the generated certificates for the kubeconfig diff --git a/go.mod b/go.mod index 8d938b99..45b6304c 100644 --- a/go.mod +++ b/go.mod @@ -28,8 +28,10 @@ require ( gopkg.in/yaml.v2 v2.4.0 helm.sh/helm/v3 v3.14.4 k8s.io/api v0.29.11 + k8s.io/apiextensions-apiserver v0.29.11 k8s.io/apimachinery v0.29.11 k8s.io/apiserver v0.29.11 + k8s.io/cli-runtime v0.29.11 k8s.io/client-go v0.29.11 k8s.io/component-base v0.29.11 k8s.io/component-helpers v0.29.11 @@ -203,8 +205,6 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.29.11 // indirect - k8s.io/cli-runtime v0.29.11 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kms v0.29.11 // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect diff --git a/pkg/apis/k3k.io/v1alpha1/types.go b/pkg/apis/k3k.io/v1alpha1/types.go index abcd347b..6462e45d 100644 --- a/pkg/apis/k3k.io/v1alpha1/types.go +++ b/pkg/apis/k3k.io/v1alpha1/types.go @@ -10,6 +10,7 @@ import ( // +kubebuilder:object:root=true // +kubebuilder:storageversion // +kubebuilder:subresource:status +// +kubebuilder:printcolumn:JSONPath=".spec.mode",name=Mode,type=string // Cluster defines a virtual Kubernetes cluster managed by k3k. // It specifies the desired state of a virtual cluster, including version, node configuration, and networking. @@ -334,7 +335,6 @@ type ClusterList struct { // +kubebuilder:subresource:status // +kubebuilder:object:root=true // +kubebuilder:printcolumn:JSONPath=".spec.allowedMode",name=Mode,type=string -// +kubebuilder:printcolumn:JSONPath=".metadata.creationTimestamp",name=Age,type=date // +kubebuilder:resource:scope=Cluster,shortName=vcp // VirtualClusterPolicy allows defining common configurations and constraints