From 4fc672acb620be1a908f4dc0d1be2b20072f8549 Mon Sep 17 00:00:00 2001 From: xrstf Date: Sat, 19 Mar 2022 19:26:09 +0100 Subject: [PATCH] allow multiple namespaces and glob expressions for namespaces and resource names --- main.go | 9 +++--- pkg/kubernetes/discovery.go | 15 ++-------- pkg/watcher/watcher.go | 59 ++++++++++++++++++++++++++++--------- 3 files changed, 51 insertions(+), 32 deletions(-) diff --git a/main.go b/main.go index 79c457e..9c986e0 100644 --- a/main.go +++ b/main.go @@ -28,7 +28,7 @@ import ( type options struct { kubeconfig string - namespace string + namespaces []string labels string hideManagedFields bool jsonPath string @@ -45,7 +45,6 @@ func main() { rootCtx := context.Background() opt := options{ - namespace: "default", hideManagedFields: true, showEmpty: false, disableWordDiff: false, @@ -53,7 +52,7 @@ func main() { } pflag.StringVar(&opt.kubeconfig, "kubeconfig", opt.kubeconfig, "kubeconfig file to use (uses $KUBECONFIG by default)") - pflag.StringVarP(&opt.namespace, "namespace", "n", opt.namespace, "Kubernetes namespace to watch resources in") + pflag.StringArrayVarP(&opt.namespaces, "namespace", "n", opt.namespaces, "Kubernetes namespace to watch resources in (supports glob expression) (can be given multiple times)") pflag.StringVarP(&opt.labels, "labels", "l", opt.labels, "Label-selector as an alternative to specifying resource names") pflag.BoolVar(&opt.hideManagedFields, "hide-managed", opt.hideManagedFields, "Do not show managed fields") pflag.StringVarP(&opt.jsonPath, "jsonpath", "j", opt.jsonPath, "JSON path expression to transform the output (applied before the --show paths)") @@ -209,7 +208,7 @@ func watchKubernetes(ctx context.Context, log logrus.FieldLogger, args []string, wg := sync.WaitGroup{} for _, gvk := range kinds { - dynamicInterface, err := kubeutil.GetDynamicInterface(gvk, appOpts.namespace, dynamicClient, mapper) + dynamicInterface, err := kubeutil.GetDynamicInterface(gvk, dynamicClient, mapper) if err != nil { log.Fatalf("Failed to create dynamic interface for %q resources: %v", gvk.Kind, err) } @@ -223,7 +222,7 @@ func watchKubernetes(ctx context.Context, log logrus.FieldLogger, args []string, wg.Add(1) go func() { - watcher.NewWatcher(printer, resourceNames).Watch(ctx, w) + watcher.NewWatcher(printer, appOpts.namespaces, resourceNames).Watch(ctx, w) wg.Done() }() } diff --git a/pkg/kubernetes/discovery.go b/pkg/kubernetes/discovery.go index 2004f61..9ec4d93 100644 --- a/pkg/kubernetes/discovery.go +++ b/pkg/kubernetes/discovery.go @@ -70,24 +70,13 @@ func computeDiscoverCacheDir(parentDir, host string) string { return filepath.Join(parentDir, safeHost) } -func GetDynamicInterface(gvk schema.GroupVersionKind, namespace string, dynamicClient dynamic.Interface, mapper meta.RESTMapper) (dynamic.ResourceInterface, error) { +func GetDynamicInterface(gvk schema.GroupVersionKind, dynamicClient dynamic.Interface, mapper meta.RESTMapper) (dynamic.ResourceInterface, error) { mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) if err != nil { return nil, fmt.Errorf("failed to determine mapping: %w", err) } - namespaced := mapping.Scope.Name() == meta.RESTScopeNameNamespace - - var dr dynamic.ResourceInterface - if namespaced { - // namespaced resources should specify the namespace - dr = dynamicClient.Resource(mapping.Resource).Namespace(namespace) - } else { - // for cluster-wide resources - dr = dynamicClient.Resource(mapping.Resource) - } - - return dr, nil + return dynamicClient.Resource(mapping.Resource), nil } func MappingFor(restMapper meta.RESTMapper, cache discovery.CachedDiscoveryInterface, resourceOrKindArg string) (*meta.RESTMapping, error) { diff --git a/pkg/watcher/watcher.go b/pkg/watcher/watcher.go index 99199f3..e5807b6 100644 --- a/pkg/watcher/watcher.go +++ b/pkg/watcher/watcher.go @@ -2,6 +2,8 @@ package watcher import ( "context" + "path/filepath" + "strings" "go.xrstf.de/stalk/pkg/diff" @@ -11,12 +13,14 @@ import ( type Watcher struct { printer *diff.Printer + namespaces []string resourceNames []string } -func NewWatcher(printer *diff.Printer, resourceNames []string) *Watcher { +func NewWatcher(printer *diff.Printer, namespaces, resourceNames []string) *Watcher { return &Watcher{ printer: printer, + namespaces: namespaces, resourceNames: resourceNames, } } @@ -28,20 +32,47 @@ func (w *Watcher) Watch(ctx context.Context, wi watch.Interface) { continue } - include := false - if len(w.resourceNames) > 0 { - for _, wantedName := range w.resourceNames { - if wantedName == obj.GetName() { - include = true - break - } - } - - if !include { - continue - } + if w.resourceNameMatches(obj) && w.resourceNamespaceMatches(obj) { + w.printer.Print(obj, event.Type) } + } +} + +func (w *Watcher) resourceNameMatches(obj *unstructured.Unstructured) bool { + // no names given, so all resources match + if len(w.resourceNames) == 0 { + return true + } + + for _, wantedName := range w.resourceNames { + if nameMatches(obj.GetName(), wantedName) { + return true + } + } + + return false +} - w.printer.Print(obj, event.Type) +func (w *Watcher) resourceNamespaceMatches(obj *unstructured.Unstructured) bool { + // no namespaces given, so all resources match + if len(w.namespaces) == 0 { + return true } + + for _, wantedNamespace := range w.namespaces { + if nameMatches(obj.GetNamespace(), wantedNamespace) { + return true + } + } + + return false +} + +func nameMatches(name string, pattern string) bool { + if strings.Contains(pattern, "*") { + matched, _ := filepath.Match(pattern, name) + return matched + } + + return name == pattern }