diff --git a/README.md b/README.md index 5972d8e..ac39a2c 100644 --- a/README.md +++ b/README.md @@ -49,12 +49,16 @@ Two modes of authentication are supported: ## Command Line Options -| Flag | Description | Default | -|-----------------|--------------------------------------|--------------------------------------------| -| `-kubeconfig` | Path to kubeconfig file | Uses in-cluster config or `~/.kube/config` | -| `-namespace` | Namespace to monitor (empty for all) | `""` (all namespaces) | -| `-workers` | Number of worker goroutines | `2` | -| `-metrics-port` | Port number for Prometheus metrics | 9090 | +| Flag | Description | Default | +|-----------------------|---------------------------------------------------------------|--------------------------------------------| +| `-kubeconfig` | Path to kubeconfig file | Uses in-cluster config or `~/.kube/config` | +| `-namespace` | Namespace to monitor (empty for all) | `""` (all namespaces) | +| `-exclude-namespaces` | Comma-separated list of namespaces to exclude (empty for all) | `""` (all namespaces) | +| `-workers` | Number of worker goroutines | `2` | +| `-metrics-port` | Port number for Prometheus metrics | 9090 | + +> [!NOTE] +> The `-namespace` and `-exclude-namespaces` flags cannot be used together. ## Environment Variables diff --git a/cmd/deployment-tracker/main.go b/cmd/deployment-tracker/main.go index b2a18f3..d4fc349 100644 --- a/cmd/deployment-tracker/main.go +++ b/cmd/deployment-tracker/main.go @@ -33,18 +33,26 @@ func getEnvOrDefault(key, defaultValue string) string { func main() { var ( - kubeconfig string - namespace string - workers int - metricsPort string + kubeconfig string + namespace string + excludeNamespaces string + workers int + metricsPort string ) flag.StringVar(&kubeconfig, "kubeconfig", "", "path to kubeconfig file (uses in-cluster config if not set)") flag.StringVar(&namespace, "namespace", "", "namespace to monitor (empty for all namespaces)") + flag.StringVar(&excludeNamespaces, "exclude-namespaces", "", "comma separated list of namespaces to exclude from monitoring (empty to include all namespaces)") flag.IntVar(&workers, "workers", 2, "number of worker goroutines") flag.StringVar(&metricsPort, "metrics-port", "9090", "port to listen to for metrics") flag.Parse() + // Cannot use both + if namespace != "" && excludeNamespaces != "" { + slog.Error("Cannot set both -namespace and -exclude-namespaces") + os.Exit(1) + } + // Validate worker count if workers < 1 || workers > 100 { slog.Error("Invalid worker count, must be between 1 and 100", @@ -143,7 +151,7 @@ func main() { cancel() }() - cntrl, err := controller.New(clientset, namespace, &cntrlCfg) + cntrl, err := controller.New(clientset, namespace, excludeNamespaces, &cntrlCfg) if err != nil { slog.Error("Failed to create controller", "error", err) diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 4a0655b..2319110 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -52,20 +52,9 @@ type Controller struct { } // New creates a new deployment tracker controller. -func New(clientset kubernetes.Interface, namespace string, cfg *Config) (*Controller, error) { +func New(clientset kubernetes.Interface, namespace string, excludeNamespaces string, cfg *Config) (*Controller, error) { // Create informer factory - var factory informers.SharedInformerFactory - if namespace == "" { - factory = informers.NewSharedInformerFactory(clientset, - 30*time.Second, - ) - } else { - factory = informers.NewSharedInformerFactoryWithOptions( - clientset, - 30*time.Second, - informers.WithNamespace(namespace), - ) - } + factory := createInformerFactory(clientset, namespace, excludeNamespaces) podInformer := factory.Core().V1().Pods().Informer() @@ -488,6 +477,56 @@ func getCacheKey(dn, digest string) string { return dn + "||" + digest } +// createInformerFactory creates a shared informer factory with the given resync period. +// If excludeNamespaces is non-empty, it will exclude those namespaces from being watched. +// If namespace is non-empty, it will only watch that namespace. +func createInformerFactory(clientset kubernetes.Interface, namespace string, excludeNamespaces string) informers.SharedInformerFactory { + var factory informers.SharedInformerFactory + switch { + case namespace != "": + slog.Info("Namespace to watch", + "namespace", + namespace, + ) + factory = informers.NewSharedInformerFactoryWithOptions( + clientset, + 30*time.Second, + informers.WithNamespace(namespace), + ) + case excludeNamespaces != "": + seenNamespaces := make(map[string]bool) + fieldSelectorParts := make([]string, 0) + + for _, ns := range strings.Split(excludeNamespaces, ",") { + ns = strings.TrimSpace(ns) + if ns != "" && !seenNamespaces[ns] { + seenNamespaces[ns] = true + fieldSelectorParts = append(fieldSelectorParts, fmt.Sprintf("metadata.namespace!=%s", ns)) + } + } + + slog.Info("Excluding namespaces from watch", + "field_selector", + strings.Join(fieldSelectorParts, ","), + ) + tweakListOptions := func(options *metav1.ListOptions) { + options.FieldSelector = strings.Join(fieldSelectorParts, ",") + } + + factory = informers.NewSharedInformerFactoryWithOptions( + clientset, + 30*time.Second, + informers.WithTweakListOptions(tweakListOptions), + ) + default: + factory = informers.NewSharedInformerFactory(clientset, + 30*time.Second, + ) + } + + return factory +} + // getARDeploymentName converts the pod's metadata into the correct format // for the deployment name for the artifact registry (this is not the same // as the K8s deployment's name!