Skip to content

Commit

Permalink
feature: create get pods for pdb command (#4)
Browse files Browse the repository at this point in the history
List all pods that are matching a given PDB selector
  • Loading branch information
dhenkel92 authored Dec 5, 2023
1 parent c3d6ca0 commit ad72b39
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 0 deletions.
1 change: 1 addition & 0 deletions cmd/kubectl-pdb/kubectl-pdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
192 changes: 192 additions & 0 deletions pkg/cmd/pods.go
Original file line number Diff line number Diff line change
@@ -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()
}
79 changes: 79 additions & 0 deletions pkg/printer/table_printer.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit ad72b39

Please sign in to comment.