This is an automated email from the ASF dual-hosted git repository.

wusheng pushed a commit to branch feat/collect-glob-pattern
in repository https://gitbox.apache.org/repos/asf/skywalking-infra-e2e.git

commit 3629c8a7414ad732aec64ca938c90f8075e01b97
Author: Wu Sheng <[email protected]>
AuthorDate: Sat Mar 28 22:01:56 2026 +0800

    feat: support glob pattern matching in collect-on-failure paths
    
    Allow paths in collect config to use shell glob patterns (*, ?, [])
    so users can match multiple folders/files flexibly, e.g. /skywalking/logs*
    or /tmp/*.hprof. Patterns are expanded inside the container via sh before
    copying. Non-glob paths behave exactly as before.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---
 internal/components/collector/collector_test.go | 41 ++++++++++++++++++++
 internal/components/collector/compose.go        | 40 ++++++++++++++++++-
 internal/components/collector/kind.go           | 51 ++++++++++++++++++++++++-
 3 files changed, 130 insertions(+), 2 deletions(-)

diff --git a/internal/components/collector/collector_test.go 
b/internal/components/collector/collector_test.go
index 08abdb7..253c6a5 100644
--- a/internal/components/collector/collector_test.go
+++ b/internal/components/collector/collector_test.go
@@ -122,6 +122,47 @@ func TestListPods_ResourceFormat(t *testing.T) {
        }
 }
 
+func TestContainsGlob(t *testing.T) {
+       tests := []struct {
+               path string
+               want bool
+       }{
+               {"/skywalking/logs/", false},
+               {"/tmp/dump.hprof", false},
+               {"/skywalking/logs*", true},
+               {"/tmp/*.hprof", true},
+               {"/tmp/dump-[0-9].hprof", true},
+               {"/var/log/?oo", true},
+       }
+       for _, tt := range tests {
+               t.Run(tt.path, func(t *testing.T) {
+                       if got := containsGlob(tt.path); got != tt.want {
+                               t.Errorf("containsGlob(%q) = %v, want %v", 
tt.path, got, tt.want)
+                       }
+               })
+       }
+}
+
+func TestExpandPodGlob_NoGlob(t *testing.T) {
+       paths, err := expandPodGlob("", "default", "pod-0", "", 
"/skywalking/logs/")
+       if err != nil {
+               t.Fatalf("unexpected error: %v", err)
+       }
+       if len(paths) != 1 || paths[0] != "/skywalking/logs/" {
+               t.Errorf("expected [/skywalking/logs/], got %v", paths)
+       }
+}
+
+func TestExpandContainerGlob_NoGlob(t *testing.T) {
+       paths, err := expandContainerGlob("abc123", "svc", "/var/log/app.log")
+       if err != nil {
+               t.Fatalf("unexpected error: %v", err)
+       }
+       if len(paths) != 1 || paths[0] != "/var/log/app.log" {
+               t.Errorf("expected [/var/log/app.log], got %v", paths)
+       }
+}
+
 func TestComposeCollectItem_NoService(t *testing.T) {
        err := composeCollectItem("/fake/compose.yml", "test-project", 
t.TempDir(), &config.CollectItem{
                Paths: []string{"/tmp"},
diff --git a/internal/components/collector/compose.go 
b/internal/components/collector/compose.go
index 09474af..abc8441 100644
--- a/internal/components/collector/compose.go
+++ b/internal/components/collector/compose.go
@@ -75,8 +75,15 @@ func composeCollectItem(composeFile, projectName, outputDir 
string, item *config
        // Collect specified files
        var errs []string
        for _, p := range item.Paths {
-               if err := collectContainerFile(outputDir, item.Service, 
containerID, p); err != nil {
+               paths, err := expandContainerGlob(containerID, item.Service, p)
+               if err != nil {
                        errs = append(errs, fmt.Sprintf("service %s path %s: 
%v", item.Service, p, err))
+                       continue
+               }
+               for _, expanded := range paths {
+                       if err := collectContainerFile(outputDir, item.Service, 
containerID, expanded); err != nil {
+                               errs = append(errs, fmt.Sprintf("service %s 
path %s: %v", item.Service, expanded, err))
+                       }
                }
        }
 
@@ -123,6 +130,37 @@ func collectContainerInspect(outputDir, service, 
containerID string) error {
        return nil
 }
 
+// expandContainerGlob expands a glob pattern inside a Docker container.
+// If the path has no glob characters it is returned as-is.
+func expandContainerGlob(containerID, service, pattern string) ([]string, 
error) {
+       if !containsGlob(pattern) {
+               return []string{pattern}, nil
+       }
+
+       cmd := fmt.Sprintf("docker exec %s sh -c 'ls -d %s 2>/dev/null'", 
containerID, pattern)
+       stdout, stderr, err := util.ExecuteCommand(cmd)
+       if err != nil {
+               logger.Log.Warnf("failed to expand glob %s in service %s: %v, 
stderr: %s", pattern, service, err, stderr)
+               return nil, fmt.Errorf("glob expansion failed for %s: %v", 
pattern, err)
+       }
+
+       var paths []string
+       for _, line := range strings.Split(strings.TrimSpace(stdout), "\n") {
+               line = strings.TrimSpace(line)
+               if line != "" {
+                       paths = append(paths, line)
+               }
+       }
+
+       if len(paths) == 0 {
+               logger.Log.Warnf("glob %s matched no files in service %s", 
pattern, service)
+               return nil, fmt.Errorf("glob %s matched no files", pattern)
+       }
+
+       logger.Log.Infof("glob %s expanded to %d path(s) in service %s", 
pattern, len(paths), service)
+       return paths, nil
+}
+
 func collectContainerFile(outputDir, service, containerID, srcPath string) 
error {
        // Preserve the full source path under the service directory to avoid 
collisions.
        // e.g. /var/log/nginx/ -> outputDir/serviceName/var/log/nginx/
diff --git a/internal/components/collector/kind.go 
b/internal/components/collector/kind.go
index 29015b9..e2a2300 100644
--- a/internal/components/collector/kind.go
+++ b/internal/components/collector/kind.go
@@ -74,8 +74,15 @@ func kindCollectItem(kubeConfigPath, outputDir string, item 
*config.CollectItem)
 
                // Collect specified files
                for _, p := range item.Paths {
-                       if err := collectPodFile(kubeConfigPath, outputDir, 
pod.namespace, pod.name, item.Container, p); err != nil {
+                       paths, err := expandPodGlob(kubeConfigPath, 
pod.namespace, pod.name, item.Container, p)
+                       if err != nil {
                                errs = append(errs, fmt.Sprintf("pod %s/%s path 
%s: %v", pod.namespace, pod.name, p, err))
+                               continue
+                       }
+                       for _, expanded := range paths {
+                               if err := collectPodFile(kubeConfigPath, 
outputDir, pod.namespace, pod.name, item.Container, expanded); err != nil {
+                                       errs = append(errs, fmt.Sprintf("pod 
%s/%s path %s: %v", pod.namespace, pod.name, expanded, err))
+                               }
                        }
                }
        }
@@ -151,6 +158,48 @@ func collectPodDescribe(kubeConfigPath, outputDir, 
namespace, podName string) er
        return nil
 }
 
+// containsGlob reports whether the path contains glob metacharacters.
+func containsGlob(path string) bool {
+       return strings.ContainsAny(path, "*?[")
+}
+
+// expandPodGlob expands a glob pattern inside a pod. If the path has no glob
+// characters it is returned as-is. Otherwise kubectl exec runs sh to expand
+// the pattern and returns the matched paths.
+func expandPodGlob(kubeConfigPath, namespace, podName, container, pattern 
string) ([]string, error) {
+       if !containsGlob(pattern) {
+               return []string{pattern}, nil
+       }
+
+       cmd := fmt.Sprintf("kubectl --kubeconfig %s -n %s exec %s", 
kubeConfigPath, namespace, podName)
+       if container != "" {
+               cmd += fmt.Sprintf(" -c %s", container)
+       }
+       cmd += fmt.Sprintf(" -- sh -c 'ls -d %s 2>/dev/null'", pattern)
+
+       stdout, stderr, err := util.ExecuteCommand(cmd)
+       if err != nil {
+               logger.Log.Warnf("failed to expand glob %s in pod %s/%s: %v, 
stderr: %s", pattern, namespace, podName, err, stderr)
+               return nil, fmt.Errorf("glob expansion failed for %s: %v", 
pattern, err)
+       }
+
+       var paths []string
+       for _, line := range strings.Split(strings.TrimSpace(stdout), "\n") {
+               line = strings.TrimSpace(line)
+               if line != "" {
+                       paths = append(paths, line)
+               }
+       }
+
+       if len(paths) == 0 {
+               logger.Log.Warnf("glob %s matched no files in pod %s/%s", 
pattern, namespace, podName)
+               return nil, fmt.Errorf("glob %s matched no files", pattern)
+       }
+
+       logger.Log.Infof("glob %s expanded to %d path(s) in pod %s/%s", 
pattern, len(paths), namespace, podName)
+       return paths, nil
+}
+
 func collectPodFile(kubeConfigPath, outputDir, namespace, podName, container, 
srcPath string) error {
        // Preserve the full source path under the pod directory to avoid 
collisions.
        // e.g. /skywalking/logs/ -> 
outputDir/namespace/podName/skywalking/logs/

Reply via email to