From 8619c0a355969bb8d6506a66fc76a3cc9a34e3ea Mon Sep 17 00:00:00 2001 From: Justin Toh Date: Thu, 3 Mar 2022 23:18:09 +0800 Subject: [PATCH] feat: Support --exclude-types flag to exclude resource types from relationship discovery Refs: #3 Signed-off-by: Justin Toh --- README.md | 1 + internal/client/client.go | 19 +++++++++++++++--- internal/client/resource.go | 28 ++++++++++++++++++++++++++ pkg/cmd/helm/flags.go | 13 +++++++++++- pkg/cmd/helm/helm.go | 40 ++++++++++++++++++++++++++++++++++--- pkg/cmd/lineage/flags.go | 13 +++++++++++- pkg/cmd/lineage/lineage.go | 24 ++++++++++++++++++++-- 7 files changed, 128 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index bb24f55..d0c3cc5 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ Flags for configuring relationship discovery parameters | `--all-namespaces`, `-A` | If present, list object relationships across all namespaces | | `--dependencies`, `-D` | If present, list object dependencies instead of dependents.
Not supported in `helm` subcommand | | `--depth`, `-d` | Maximum depth to find relationships | +| `--exclude-types` | Accepts a comma separated list of resource types to exclude from relationship discovery.
You can also use multiple flag options like --exclude-types type1 --exclude-types type2... | | `--scopes`, `-S` | Accepts a comma separated list of additional namespaces to find relationships.
You can also use multiple flag options like -S namespace1 -S namespace2... | Flags for configuring output format diff --git a/internal/client/client.go b/internal/client/client.go index 6f821ab..09366a7 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -40,8 +40,9 @@ type GetTableOptions struct { } type ListOptions struct { - APIResources []APIResource - Namespaces []string + APIResourcesToExclude []APIResource + APIResourcesToInclude []APIResource + Namespaces []string } type Interface interface { @@ -218,7 +219,7 @@ func decodeIntoTable(obj runtime.Object) (*metav1.Table, error) { func (c *client) List(ctx context.Context, opts ListOptions) (*unstructuredv1.UnstructuredList, error) { klog.V(4).Infof("List with options: %+v", opts) var err error - apis := opts.APIResources + apis := opts.APIResourcesToInclude if len(apis) == 0 { apis, err = c.GetAPIResources(ctx) if err != nil { @@ -226,6 +227,18 @@ func (c *client) List(ctx context.Context, opts ListOptions) (*unstructuredv1.Un } } + // Exclude resources + if len(opts.APIResourcesToExclude) > 0 { + excludeGKSet := ResourcesToGroupKindSet(opts.APIResourcesToExclude) + newAPIs := []APIResource{} + for _, api := range apis { + if _, ok := excludeGKSet[api.GroupKind()]; !ok { + newAPIs = append(newAPIs, api) + } + } + apis = newAPIs + } + // Deduplicate list of namespaces & determine the scope for listing objects isClusterScopeRequest, nsSet := false, make(map[string]struct{}) if len(opts.Namespaces) == 0 { diff --git a/internal/client/resource.go b/internal/client/resource.go index 377066c..2f8059a 100644 --- a/internal/client/resource.go +++ b/internal/client/resource.go @@ -10,6 +10,13 @@ import ( // APIResource represents a Kubernetes API resource. type APIResource metav1.APIResource +func (r APIResource) GroupKind() schema.GroupKind { + return schema.GroupKind{ + Group: r.Group, + Kind: r.Kind, + } +} + func (r APIResource) GroupVersionKind() schema.GroupVersionKind { return schema.GroupVersionKind{ Group: r.Group, @@ -40,6 +47,27 @@ func (r APIResource) WithGroupString() string { return r.Name + "." + r.Group } +func ResourcesToGroupKindSet(apis []APIResource) map[schema.GroupKind]struct{} { + gkSet := map[schema.GroupKind]struct{}{} + for _, api := range apis { + gk := api.GroupKind() + // Account for resources that migrated API groups (for Kubernetes v1.18 & above) + switch { + // migrated from "events.v1" to "events.v1.events.k8s.io" + case gk.Kind == "Event" && (gk.Group == "" || gk.Group == "events.k8s.io"): + gkSet[schema.GroupKind{Kind: gk.Kind, Group: ""}] = struct{}{} + gkSet[schema.GroupKind{Kind: gk.Kind, Group: "events.k8s.io"}] = struct{}{} + // migrated from "ingresses.v1.extensions" to "ingresses.v1.networking.k8s.io" + case gk.Kind == "Ingress" && (gk.Group == "extensions" || gk.Group == "networking.k8s.io"): + gkSet[schema.GroupKind{Kind: gk.Kind, Group: "extensions"}] = struct{}{} + gkSet[schema.GroupKind{Kind: gk.Kind, Group: "networking.k8s.io"}] = struct{}{} + default: + gkSet[gk] = struct{}{} + } + } + return gkSet +} + // ObjectMeta contains the metadata for identifying a Kubernetes object. type ObjectMeta struct { APIResource diff --git a/pkg/cmd/helm/flags.go b/pkg/cmd/helm/flags.go index 722091c..3224d4d 100644 --- a/pkg/cmd/helm/flags.go +++ b/pkg/cmd/helm/flags.go @@ -1,6 +1,8 @@ package helm import ( + "fmt" + "github.com/spf13/cobra" "github.com/spf13/pflag" cmdutil "k8s.io/kubectl/pkg/cmd/util" @@ -11,6 +13,7 @@ import ( const ( flagAllNamespaces = "all-namespaces" flagAllNamespacesShorthand = "A" + flagExcludeTypes = "exclude-types" flagDepth = "depth" flagDepthShorthand = "d" flagScopes = "scopes" @@ -21,6 +24,7 @@ const ( type Flags struct { AllNamespaces *bool Depth *uint + ExcludeTypes *[]string Scopes *[]string } @@ -39,8 +43,13 @@ func (f *Flags) AddFlags(flags *pflag.FlagSet) { if f.Depth != nil { flags.UintVarP(f.Depth, flagDepth, flagDepthShorthand, *f.Depth, "Maximum depth to find relationships") } + if f.ExcludeTypes != nil { + excludeTypesUsage := fmt.Sprintf("Accepts a comma separated list of resource types to exclude from relationship discovery. You can also use multiple flag options like --%s resource1 --%s resource2...", flagExcludeTypes, flagExcludeTypes) + flags.StringSliceVar(f.ExcludeTypes, flagExcludeTypes, *f.ExcludeTypes, excludeTypesUsage) + } if f.Scopes != nil { - flags.StringSliceVarP(f.Scopes, flagScopes, flagScopesShorthand, *f.Scopes, "Accepts a comma separated list of additional namespaces to find relationships. You can also use multiple flag options like -S namespace1 -S namespace2...") + scopesUsage := fmt.Sprintf("Accepts a comma separated list of additional namespaces to find relationships. You can also use multiple flag options like -%s namespace1 -%s namespace2...", flagScopesShorthand, flagScopesShorthand) + flags.StringSliceVarP(f.Scopes, flagScopes, flagScopesShorthand, *f.Scopes, scopesUsage) } } @@ -59,11 +68,13 @@ func (*Flags) RegisterFlagCompletionFunc(cmd *cobra.Command, f cmdutil.Factory) func NewFlags() *Flags { allNamespaces := false depth := uint(0) + excludeTypes := []string{} scopes := []string{} return &Flags{ AllNamespaces: &allNamespaces, Depth: &depth, + ExcludeTypes: &excludeTypes, Scopes: &scopes, } } diff --git a/pkg/cmd/helm/helm.go b/pkg/cmd/helm/helm.go index 1756b81..c39180b 100644 --- a/pkg/cmd/helm/helm.go +++ b/pkg/cmd/helm/helm.go @@ -41,6 +41,9 @@ var ( # List all resources associated with release named "bar" & the corresponding relationship type(s) %CMD_PATH% bar --output=wide + # List all resources associated with release named "bar", excluding event & secret resource types + %CMD_PATH% pv/disk --dependencies --exclude-types=ev,secret + # List only resources provisioned by the release named "bar" %CMD_PATH% bar --depth=1`) cmdShort = "Display resources associated with a Helm release & their dependents" @@ -169,6 +172,7 @@ func (o *CmdOptions) Validate() error { klog.V(4).Infof("RequestRelease: %v", o.RequestRelease) klog.V(4).Infof("Flags.AllNamespaces: %t", *o.Flags.AllNamespaces) klog.V(4).Infof("Flags.Depth: %v", *o.Flags.Depth) + klog.V(4).Infof("Flags.ExcludeTypes: %v", *o.Flags.ExcludeTypes) klog.V(4).Infof("Flags.Scopes: %v", *o.Flags.Scopes) klog.V(4).Infof("ClientFlags.Context: %s", *o.ClientFlags.Context) klog.V(4).Infof("ClientFlags.Namespace: %s", *o.ClientFlags.Namespace) @@ -182,7 +186,7 @@ func (o *CmdOptions) Validate() error { } // Run implements all the necessary functionality for the helm command. -//nolint:funlen +//nolint:funlen,gocognit func (o *CmdOptions) Run() error { ctx := context.Background() @@ -213,6 +217,33 @@ func (o *CmdOptions) Run() error { return err } + // Determine resources to list + excludeAPIs := []client.APIResource{} + if o.Flags.ExcludeTypes != nil { + for _, kind := range *o.Flags.ExcludeTypes { + api, err := o.Client.ResolveAPIResource(kind) + if err != nil { + return err + } + excludeAPIs = append(excludeAPIs, *api) + } + } + + // Filter out objects that matches any excluded resource + if len(excludeAPIs) > 0 { + excludeGKSet := client.ResourcesToGroupKindSet(excludeAPIs) + newRlsObjs := []unstructuredv1.Unstructured{} + for _, i := range rlsObjs { + if _, ok := excludeGKSet[i.GroupVersionKind().GroupKind()]; !ok { + newRlsObjs = append(newRlsObjs, i) + } + } + rlsObjs = newRlsObjs + if _, ok := excludeGKSet[stgObj.GroupVersionKind().GroupKind()]; ok { + stgObj = nil + } + } + // Determine the namespaces to list objects var namespaces []string nsSet := map[string]struct{}{o.Namespace: {}} @@ -229,8 +260,11 @@ func (o *CmdOptions) Run() error { namespaces = append(namespaces, *o.Flags.Scopes...) } - // Fetch all resources in the cluster - objs, err := o.Client.List(ctx, client.ListOptions{Namespaces: namespaces}) + // Fetch resources in the cluster + objs, err := o.Client.List(ctx, client.ListOptions{ + APIResourcesToExclude: excludeAPIs, + Namespaces: namespaces, + }) if err != nil { return err } diff --git a/pkg/cmd/lineage/flags.go b/pkg/cmd/lineage/flags.go index fcd8ccb..214740a 100644 --- a/pkg/cmd/lineage/flags.go +++ b/pkg/cmd/lineage/flags.go @@ -1,6 +1,8 @@ package lineage import ( + "fmt" + "github.com/spf13/cobra" "github.com/spf13/pflag" cmdutil "k8s.io/kubectl/pkg/cmd/util" @@ -11,6 +13,7 @@ import ( const ( flagAllNamespaces = "all-namespaces" flagAllNamespacesShorthand = "A" + flagExcludeTypes = "exclude-types" flagDepth = "depth" flagDepthShorthand = "d" flagScopes = "scopes" @@ -24,6 +27,7 @@ type Flags struct { AllNamespaces *bool Dependencies *bool Depth *uint + ExcludeTypes *[]string Scopes *[]string } @@ -45,8 +49,13 @@ func (f *Flags) AddFlags(flags *pflag.FlagSet) { if f.Depth != nil { flags.UintVarP(f.Depth, flagDepth, flagDepthShorthand, *f.Depth, "Maximum depth to find relationships") } + if f.ExcludeTypes != nil { + excludeTypesUsage := fmt.Sprintf("Accepts a comma separated list of resource types to exclude from relationship discovery. You can also use multiple flag options like --%s kind1 --%s kind1...", flagExcludeTypes, flagExcludeTypes) + flags.StringSliceVar(f.ExcludeTypes, flagExcludeTypes, *f.ExcludeTypes, excludeTypesUsage) + } if f.Scopes != nil { - flags.StringSliceVarP(f.Scopes, flagScopes, flagScopesShorthand, *f.Scopes, "Accepts a comma separated list of additional namespaces to find relationships. You can also use multiple flag options like -S namespace1 -S namespace2...") + scopesUsage := fmt.Sprintf("Accepts a comma separated list of additional namespaces to find relationships. You can also use multiple flag options like -%s namespace1 -%s namespace2...", flagScopesShorthand, flagScopesShorthand) + flags.StringSliceVarP(f.Scopes, flagScopes, flagScopesShorthand, *f.Scopes, scopesUsage) } } @@ -66,12 +75,14 @@ func NewFlags() *Flags { allNamespaces := false dependencies := false depth := uint(0) + excludeTypes := []string{} scopes := []string{} return &Flags{ AllNamespaces: &allNamespaces, Dependencies: &dependencies, Depth: &depth, + ExcludeTypes: &excludeTypes, Scopes: &scopes, } } diff --git a/pkg/cmd/lineage/lineage.go b/pkg/cmd/lineage/lineage.go index 41dc36a..7857809 100644 --- a/pkg/cmd/lineage/lineage.go +++ b/pkg/cmd/lineage/lineage.go @@ -34,6 +34,9 @@ var ( # List all dependents of the node named "k3d-dev-server" & the corresponding relationship type(s) %CMD_PATH% node/k3d-dev-server --output=wide + # List all dependents of the persistentvolume named "disk", excluding event & secret resource types + %CMD_PATH% pv/disk --dependencies --exclude-types=ev,secret + # List all dependencies of the pod named "bar-5cc79d4bf5-xgvkc" %CMD_PATH% pod.v1. bar-5cc79d4bf5-xgvkc --dependencies @@ -172,6 +175,7 @@ func (o *CmdOptions) Validate() error { klog.V(4).Infof("Flags.AllNamespaces: %t", *o.Flags.AllNamespaces) klog.V(4).Infof("Flags.Dependencies: %t", *o.Flags.Dependencies) klog.V(4).Infof("Flags.Depth: %v", *o.Flags.Depth) + klog.V(4).Infof("Flags.ExcludeTypes: %v", *o.Flags.ExcludeTypes) klog.V(4).Infof("Flags.Scopes: %v", *o.Flags.Scopes) klog.V(4).Infof("ClientFlags.Context: %s", *o.ClientFlags.Context) klog.V(4).Infof("ClientFlags.Namespace: %s", *o.ClientFlags.Namespace) @@ -185,6 +189,7 @@ func (o *CmdOptions) Validate() error { } // Run implements all the necessary functionality for the lineage command. +//nolint:funlen func (o *CmdOptions) Run() error { ctx := context.Background() @@ -211,6 +216,18 @@ func (o *CmdOptions) Run() error { return err } + // Determine resources to list + excludeAPIs := []client.APIResource{} + if o.Flags.ExcludeTypes != nil { + for _, kind := range *o.Flags.ExcludeTypes { + api, err := o.Client.ResolveAPIResource(kind) + if err != nil { + return err + } + excludeAPIs = append(excludeAPIs, *api) + } + } + // Determine the namespaces to list objects namespaces := []string{o.Namespace} if o.Flags.AllNamespaces != nil && *o.Flags.AllNamespaces { @@ -220,8 +237,11 @@ func (o *CmdOptions) Run() error { namespaces = append(namespaces, *o.Flags.Scopes...) } - // Fetch all resources in the cluster - objs, err := o.Client.List(ctx, client.ListOptions{Namespaces: namespaces}) + // Fetch resources in the cluster + objs, err := o.Client.List(ctx, client.ListOptions{ + APIResourcesToExclude: excludeAPIs, + Namespaces: namespaces, + }) if err != nil { return err }