From ad72b39ad93747f0e8b19a162a4ee2f5a3921c72 Mon Sep 17 00:00:00 2001 From: Daniel Henkel <9447057+dhenkel92@users.noreply.github.com> Date: Tue, 5 Dec 2023 13:50:26 +0100 Subject: [PATCH] feature: create get pods for pdb command (#4) List all pods that are matching a given PDB selector --- cmd/kubectl-pdb/kubectl-pdb.go | 1 + go.mod | 1 + go.sum | 3 + pkg/cmd/pods.go | 192 +++++++++++++++++++++++++++++++++ pkg/printer/table_printer.go | 79 ++++++++++++++ 5 files changed, 276 insertions(+) create mode 100644 pkg/cmd/pods.go create mode 100644 pkg/printer/table_printer.go diff --git a/cmd/kubectl-pdb/kubectl-pdb.go b/cmd/kubectl-pdb/kubectl-pdb.go index 51cee08..f2ed96d 100644 --- a/cmd/kubectl-pdb/kubectl-pdb.go +++ b/cmd/kubectl-pdb/kubectl-pdb.go @@ -24,6 +24,7 @@ func main() { } rootCmd.AddCommand(cmd.NewCmdPdb(streams, conf)) rootCmd.AddCommand(cmd.NewCmdCreatePDB(streams, conf)) + rootCmd.AddCommand(cmd.NewCmdPods(streams, conf)) if err := rootCmd.Execute(); err != nil { os.Exit(1) diff --git a/go.mod b/go.mod index 50462fd..0074011 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( k8s.io/apimachinery v0.28.4 k8s.io/cli-runtime v0.28.4 k8s.io/client-go v0.28.4 + k8s.io/klog v1.0.0 ) require ( diff --git a/go.sum b/go.sum index 17e1921..8f897ed 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,7 @@ github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= @@ -195,6 +196,8 @@ k8s.io/cli-runtime v0.28.4 h1:IW3aqSNFXiGDllJF4KVYM90YX4cXPGxuCxCVqCD8X+Q= k8s.io/cli-runtime v0.28.4/go.mod h1:MLGRB7LWTIYyYR3d/DOgtUC8ihsAPA3P8K8FDNIqJ0k= k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY= k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= k8s.io/kube-openapi v0.0.0-20231129212854-f0671cc7e66a h1:ZeIPbyHHqahGIbeyLJJjAUhnxCKqXaDY+n89Ms8szyA= diff --git a/pkg/cmd/pods.go b/pkg/cmd/pods.go new file mode 100644 index 0000000..dc72b2e --- /dev/null +++ b/pkg/cmd/pods.go @@ -0,0 +1,192 @@ +package cmd + +import ( + "context" + "fmt" + "strings" + + "github.com/dhenkel92/kubectl-utils/pkg/kube" + printerUtils "github.com/dhenkel92/kubectl-utils/pkg/printer" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/printers" + "k8s.io/client-go/rest" +) + +// TODO: add sort-by argument +type PodsOptions struct { + genericclioptions.IOStreams + + configFlags *genericclioptions.ConfigFlags + Namespace string + PDBName string + Output string + Template string + + OutputWide bool +} + +func NewPodsOptions(streams genericclioptions.IOStreams, conf *genericclioptions.ConfigFlags) *PodsOptions { + return &PodsOptions{ + IOStreams: streams, + configFlags: conf, + } +} + +func NewCmdPods(streams genericclioptions.IOStreams, conf *genericclioptions.ConfigFlags) *cobra.Command { + o := NewPodsOptions(streams, conf) + + cmd := &cobra.Command{ + Use: "pods [pdb name]", + Short: "List pods", + Long: "List pods", + RunE: func(cmd *cobra.Command, args []string) error { + if err := o.Complete(cmd, args); err != nil { + return err + } + if err := o.Validate(); err != nil { + return err + } + if err := o.Run(); err != nil { + return err + } + return nil + }, + } + + flags := cmd.Flags() + flags.StringVarP(&o.Output, "output", "o", "human", "[human, json, yaml, jsonpath]") + + return cmd +} + +func (o *PodsOptions) Complete(cmd *cobra.Command, args []string) error { + var err error + + if len(args) < 1 { + return fmt.Errorf("pdb name is required") + } + o.PDBName = args[0] + + o.Namespace, _, err = o.configFlags.ToRawKubeConfigLoader().Namespace() + if err != nil { + return err + } + + if strings.HasPrefix(o.Output, "jsonpath") { + split := strings.Split(o.Output, "=") + o.Output = split[0] + o.Template = split[1] + } + if o.Output == "wide" { + o.OutputWide = true + } + return nil +} + +func (o *PodsOptions) Validate() error { + switch o.Output { + case "json", "yaml", "human", "jsonpath", "wide": + default: + return fmt.Errorf("invalid output '%s'", o.Output) + } + + return nil +} + +func (o *PodsOptions) GetPrinter() (printers.ResourcePrinter, error) { + switch o.Output { + case "jsonpath": + return printers.NewJSONPathPrinter(o.Template) + case "json": + return &printers.JSONPrinter{}, nil + case "yaml": + return &printers.YAMLPrinter{}, nil + } + + tablePrinter := printers.NewTablePrinter(printers.PrintOptions{ + WithKind: false, + NoHeaders: false, + Wide: o.OutputWide, + WithNamespace: false, + ShowLabels: false, + ColumnLabels: []string{}, + }) + return &printerUtils.TablePrinter{Delegate: tablePrinter}, nil +} + +func (o *PodsOptions) transformRequests(req *rest.Request) { + if o.Output != "human" { + return + } + req.SetHeader("Accept", strings.Join([]string{ + fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1.SchemeGroupVersion.Version, metav1.GroupName), + fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName), + "application/json", + }, ",")) + + // TODO: sorting + // if sorting, ensure we receive the full object in order to introspect its fields via jsonpath + // if len(o.SortBy) > 0 { + // req.Param("includeObject", "Object") + // } +} + +func (o *PodsOptions) Run() error { + ctx := context.Background() + printer, err := o.GetPrinter() + if err != nil { + return err + } + + kubeClient, err := kube.New(o.configFlags) + if err != nil { + return err + } + + pdb, err := kubeClient.GetClientset().PolicyV1().PodDisruptionBudgets(o.Namespace).Get(ctx, o.PDBName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + fmt.Println(err.Error()) + return nil + } + return err + } + + selector, err := metav1.LabelSelectorAsSelector(pdb.Spec.Selector) + if err != nil { + return err + } + + r := kubeClient.NewBuilder().Unstructured(). + NamespaceParam(o.Namespace).DefaultNamespace(). + ResourceTypeOrNameArgs(true, "pods"). + FieldSelectorParam(""). + LabelSelector(selector.String()). + ContinueOnError(). + Latest(). + Flatten(). + TransformRequests(o.transformRequests). + Do() + + if err := r.Err(); err != nil { + return err + } + + infos, err := r.Infos() + if err != nil { + return err + } + + w := printers.GetNewTabWriter(o.Out) + for _, pod := range infos { + if err := printer.PrintObj(pod.Object, w); err != nil { + return err + } + } + + return w.Flush() +} diff --git a/pkg/printer/table_printer.go b/pkg/printer/table_printer.go new file mode 100644 index 0000000..87f175e --- /dev/null +++ b/pkg/printer/table_printer.go @@ -0,0 +1,79 @@ +// Inspired by: https://github.com/dhenkel92/kubernetes/blob/d61cbac69aae97db1839bd2e0e86d68f26b353a7/staging/src/k8s.io/kubectl/pkg/cmd/get/table_printer.go +package printer + +import ( + "fmt" + "io" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/printers" + "k8s.io/klog" +) + +// TablePrinter decodes table objects into typed objects before delegating to another printer. +// Non-table types are simply passed through +type TablePrinter struct { + Delegate printers.ResourcePrinter +} + +func (t *TablePrinter) PrintObj(obj runtime.Object, writer io.Writer) error { + table, err := decodeIntoTable(obj) + if err == nil { + return t.Delegate.PrintObj(table, writer) + } + // if we are unable to decode server response into a v1beta1.Table, + // fallback to client-side printing with whatever info the server returned. + klog.V(2).Infof("Unable to decode server response into a Table. Falling back to hardcoded types: %v", err) + return t.Delegate.PrintObj(obj, writer) +} + +var recognizedTableVersions = map[schema.GroupVersionKind]bool{ + metav1beta1.SchemeGroupVersion.WithKind("Table"): true, + metav1.SchemeGroupVersion.WithKind("Table"): true, +} + +// assert the types are identical, since we're decoding both types into a metav1.Table +var _ metav1.Table = metav1beta1.Table{} +var _ metav1beta1.Table = metav1.Table{} + +func decodeIntoTable(obj runtime.Object) (runtime.Object, error) { + event, isEvent := obj.(*metav1.WatchEvent) + if isEvent { + obj = event.Object.Object + } + + if !recognizedTableVersions[obj.GetObjectKind().GroupVersionKind()] { + return nil, fmt.Errorf("attempt to decode non-Table object") + } + + unstr, ok := obj.(*unstructured.Unstructured) + if !ok { + return nil, fmt.Errorf("attempt to decode non-Unstructured object") + } + table := &metav1.Table{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstr.Object, table); err != nil { + return nil, err + } + + for i := range table.Rows { + row := &table.Rows[i] + if row.Object.Raw == nil || row.Object.Object != nil { + continue + } + converted, err := runtime.Decode(unstructured.UnstructuredJSONScheme, row.Object.Raw) + if err != nil { + return nil, err + } + row.Object.Object = converted + } + + if isEvent { + event.Object.Object = table + return event, nil + } + return table, nil +}