diff --git a/modules/cli/cmd/logs.go b/modules/cli/cmd/logs.go index c68015eb9..31e0ef9a5 100644 --- a/modules/cli/cmd/logs.go +++ b/modules/cli/cmd/logs.go @@ -228,12 +228,15 @@ var logsCmd = &cobra.Command{ after, _ := flags.GetString("after") before, _ := flags.GetString("before") + allNamespaces, _ := flags.GetBool("all-namespaces") + grep, _ := flags.GetString("grep") regionList, _ := flags.GetStringSlice("region") zoneList, _ := flags.GetStringSlice("zone") osList, _ := flags.GetStringSlice("os") archList, _ := flags.GetStringSlice("arch") nodeList, _ := flags.GetStringSlice("node") + namespace, _ := flags.GetString("namespace") hideHeader, _ := flags.GetBool("hide-header") hideTs, _ := flags.GetBool("hide-ts") @@ -327,6 +330,8 @@ var logsCmd = &cobra.Command{ logs.WithOSes(osList), logs.WithArches(archList), logs.WithNodes(nodeList), + logs.WithNamespace(namespace), + logs.WithAllNamespaces(allNamespaces), } switch streamMode { @@ -633,6 +638,7 @@ func init() { logsCmd.MarkFlagsMutuallyExclusive("until", "before") flagset.StringP("grep", "g", "", "Filter records by a regular expression") + flagset.StringP("namespace", "n", "", "Filter source pods by namespace") flagset.StringSlice("region", []string{}, "Filter source pods by region") flagset.StringSlice("zone", []string{}, "Filter source pods by zone") @@ -641,6 +647,7 @@ func init() { flagset.StringSlice("node", []string{}, "Filter source pods by node name") flagset.Bool("raw", false, "Output only raw log messages without metadata") + flagset.Bool("all-namespaces", false, "Include records from all namespaces (overrides --namespace)") flagset.Bool("hide-ts", false, "Hide the timestamp of each record") flagset.Bool("with-node", false, "Show the source node of each record") flagset.Bool("with-region", false, "Show the source region of each record") @@ -655,7 +662,7 @@ func init() { flagset.Bool("hide-header", false, "Hide table header") flagset.Bool("hide-dot", false, "Hide the dot indicator in the records") - //flagset.BoolP("reverse", "r", false, "List records in reverse order") + // flagset.BoolP("reverse", "r", false, "List records in reverse order") flagset.Bool("force", false, "Force command (if necessary)") diff --git a/modules/shared/k8shelpers/connection-manager.go b/modules/shared/k8shelpers/connection-manager.go index 2bda77014..23fa4193d 100644 --- a/modules/shared/k8shelpers/connection-manager.go +++ b/modules/shared/k8shelpers/connection-manager.go @@ -51,6 +51,7 @@ type ConnectionManager interface { GetOrCreateClientset(kubeContext string) (kubernetes.Interface, error) GetOrCreateDynamicClient(kubeContext string) (dynamic.Interface, error) GetDefaultNamespace(kubeContext string) string + GetNamespaceList(kubeContext string) ([]string, error) DerefKubeContext(kubeContext *string) string NewInformer(ctx context.Context, kubeContext string, token string, namespace string, gvr schema.GroupVersionResource) (informers.GenericInformer, func(), error) WaitUntilReady(ctx context.Context, kubeContext string) error @@ -230,6 +231,26 @@ func (cm *DesktopConnectionManager) GetDefaultNamespace(kubeContext string) stri return context.Namespace } +// GetNamespaceList returns a list of all namespaces in the cluster +func (cm *DesktopConnectionManager) GetNamespaceList(kubeContext string) ([]string, error) { + clientset, err := cm.GetOrCreateClientset(kubeContext) + if err != nil { + return nil, err + } + + rawNamespaceList, err := clientset.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{}) + if err != nil { + return nil, err + } + + namespaceList := make([]string, len(rawNamespaceList.Items)) + for i, val := range rawNamespaceList.Items { + namespaceList[i] = val.Name + } + + return namespaceList, nil +} + // DerefKubeContext func (cm *DesktopConnectionManager) DerefKubeContext(kubeContextPtr *string) string { cm.mu.Lock() @@ -516,6 +537,26 @@ func (cm *InClusterConnectionManager) GetDefaultNamespace(kubeContext string) st return metav1.NamespaceDefault } +// GetNamespaceList returns a list of all namespaces in the cluster +func (cm *InClusterConnectionManager) GetNamespaceList(kubeContext string) ([]string, error) { + clientset, err := cm.GetOrCreateClientset(kubeContext) + if err != nil { + return nil, err + } + + rawNamespaceList, err := clientset.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{}) + if err != nil { + return nil, err + } + + namespaceList := make([]string, len(rawNamespaceList.Items)) + for i, val := range rawNamespaceList.Items { + namespaceList[i] = val.Name + } + + return namespaceList, nil +} + // DerefKubeContext func (cm *InClusterConnectionManager) DerefKubeContext(kubeContext *string) string { return "" diff --git a/modules/shared/k8shelpers/mock/connection-manager.go b/modules/shared/k8shelpers/mock/connection-manager.go index c2b253e7f..a8a6b6146 100644 --- a/modules/shared/k8shelpers/mock/connection-manager.go +++ b/modules/shared/k8shelpers/mock/connection-manager.go @@ -84,6 +84,17 @@ func (m *MockConnectionManager) GetDefaultNamespace(kubeContext string) string { return ret.String(0) } +func (m *MockConnectionManager) GetNamespaceList(kubeContext string) ([]string, error) { + ret := m.Called(kubeContext) + + var r0 []string + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + + return r0, ret.Error(1) +} + func (m *MockConnectionManager) DerefKubeContext(kubeContextPtr *string) string { ret := m.Called(kubeContextPtr) return ret.String(0) diff --git a/modules/shared/logs/options.go b/modules/shared/logs/options.go index 63d0fd8d4..0fe9b8422 100644 --- a/modules/shared/logs/options.go +++ b/modules/shared/logs/options.go @@ -237,3 +237,25 @@ func WithAllowedNamespaces(allowedNamespaces []string) Option { return nil } } + +// WithNamespace sets the namespace for source watcher +func WithNamespace(namespace string) Option { + return func(target any) error { + switch t := target.(type) { + case *sourceWatcher: + t.namespace = namespace + } + return nil + } +} + +// WithAllNamespaces sets whether to return logs from all namespaces +func WithAllNamespaces(allNamespaces bool) Option { + return func(target any) error { + switch t := target.(type) { + case *sourceWatcher: + t.allNamespaces = allNamespaces + } + return nil + } +} diff --git a/modules/shared/logs/source-watcher.go b/modules/shared/logs/source-watcher.go index ee9974453..452c796a5 100644 --- a/modules/shared/logs/source-watcher.go +++ b/modules/shared/logs/source-watcher.go @@ -79,14 +79,16 @@ type sourceWatcher struct { cm k8shelpers.ConnectionManager eventbus evbus.Bus - kubeContext string - bearerToken string - regions []string - zones []string - oses []string - arches []string - nodes []string - containers []string + kubeContext string + bearerToken string + namespace string + allNamespaces bool + regions []string + zones []string + oses []string + arches []string + nodes []string + containers []string allowedNamespaces []string parsedPaths []parsedPath @@ -122,22 +124,29 @@ func NewSourceWatcher(cm k8shelpers.ConnectionManager, sourcePaths []string, opt // Get default namespace defaultNamespace := cm.GetDefaultNamespace(sw.kubeContext) - // Parse paths - parsedPaths := []parsedPath{} - for _, p := range sourcePaths { - pp, err := parsePath(p, defaultNamespace) - if err != nil { - return nil, err - } + // Get list of all namespaces + namespaceList, err := cm.GetNamespaceList(sw.kubeContext) + if err != nil { + return nil, err + } + + parsedPaths, err := parsePaths(sourcePaths, defaultNamespace, sw.allNamespaces, namespaceList, sw.namespace) + + if err != nil { + return nil, err + } + + // Filter paths that do not match allowed namespaces + filteredPaths := []parsedPath{} + for _, pp := range parsedPaths { - // Remove paths that do not match allowed namespaces if len(sw.allowedNamespaces) > 0 && !slices.Contains(sw.allowedNamespaces, pp.Namespace) { continue } - parsedPaths = append(parsedPaths, pp) + filteredPaths = append(filteredPaths, pp) } - sw.parsedPaths = parsedPaths + sw.parsedPaths = filteredPaths return sw, nil } @@ -474,6 +483,48 @@ type parsedPath struct { ContainerName string } +// Parse multiple source paths with namespace priority logic +func parsePaths(sourcePaths []string, defaultNamespace string, allNamespaces bool, namespaceList []string, namespace string) ([]parsedPath, error) { + var result []parsedPath + + // Check if any source path has explicit namespace (highest priority) + sourceHasNamespace := false + for _, path := range sourcePaths { + if strings.Contains(path, ":") { + sourceHasNamespace = true + break + } + } + + if allNamespaces && !sourceHasNamespace { + for _, namespace := range namespaceList { + for _, path := range sourcePaths { + pp, err := parsePath(path, namespace) + if err != nil { + return nil, err + } + result = append(result, pp) + } + } + } else { + // using namespace flag if it exists and source paths do not have explicit namespace + if namespace != "" && !sourceHasNamespace { + defaultNamespace = namespace + } + + for _, path := range sourcePaths { + pp, err := parsePath(path, defaultNamespace) + if err != nil { + return nil, err + } + result = append(result, pp) + } + + } + + return result, nil +} + // Parse source path func parsePath(path string, defaultNamespace string) (parsedPath, error) { // Remove leading and trailing slashes diff --git a/modules/shared/logs/stream.go b/modules/shared/logs/stream.go index add86221f..bf595eb7c 100644 --- a/modules/shared/logs/stream.go +++ b/modules/shared/logs/stream.go @@ -50,7 +50,7 @@ type Stream struct { sinceTime time.Time untilTime time.Time - //reverse bool + // reverse bool follow bool grep string grepRegex *regexp.Regexp