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

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


The following commit(s) were added to refs/heads/main by this push:
     new e26033e  feat: support glob pattern matching in collect-on-failure 
paths (#142)
e26033e is described below

commit e26033e1faaf865899c486ffe17dabdf17b90aae
Author: 吴晟 Wu Sheng <[email protected]>
AuthorDate: Sat Mar 28 22:41:40 2026 +0800

    feat: support glob pattern matching in collect-on-failure paths (#142)
    
      - Add shell glob pattern support (*, ?, [...]) for paths in 
collect-on-failure config
      - Patterns are expanded inside the container/pod via sh -c 'ls -d -- 
<pattern> 2>/dev/null || true' before copying
      - Non-glob paths behave exactly as before
---
 internal/components/collector/collector_test.go | 70 +++++++++++++++++++
 internal/components/collector/compose.go        | 44 +++++++++++-
 internal/components/collector/kind.go           | 92 ++++++++++++++++++++++++-
 3 files changed, 204 insertions(+), 2 deletions(-)

diff --git a/internal/components/collector/collector_test.go 
b/internal/components/collector/collector_test.go
index 08abdb7..786e6e0 100644
--- a/internal/components/collector/collector_test.go
+++ b/internal/components/collector/collector_test.go
@@ -122,6 +122,76 @@ func TestListPods_ResourceFormat(t *testing.T) {
        }
 }
 
+func TestContainsGlob(t *testing.T) {
+       tests := []struct {
+               path string
+               want bool
+       }{
+               {"/skywalking/logs/", false},
+               {"/tmp/dump.hprof", false},
+               {"/tmp/app[1].log", true},  // [1] is a valid shell character 
class
+               {"/tmp/app[].log", false},  // [] is not a valid character class
+               {"/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 TestValidateGlobPattern(t *testing.T) {
+       tests := []struct {
+               pattern string
+               wantErr bool
+       }{
+               {"/skywalking/logs*", false},
+               {"/tmp/*.hprof", false},
+               {"/tmp/dump-[0-9].hprof", false},
+               {"/var/log/app-?.log", false},
+               {"'; rm -rf /; '", true},
+               {"/path with spaces/*", true},
+               {"/tmp/$(whoami)", true},
+               {"/tmp/`id`", true},
+               {"/tmp/foo|bar", true},
+               {"/tmp/foo;bar", true},
+               {"/tmp/foo&bar", true},
+       }
+       for _, tt := range tests {
+               t.Run(tt.pattern, func(t *testing.T) {
+                       err := validateGlobPattern(tt.pattern)
+                       if (err != nil) != tt.wantErr {
+                               t.Errorf("validateGlobPattern(%q) error = %v, 
wantErr %v", tt.pattern, err, tt.wantErr)
+                       }
+               })
+       }
+}
+
+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..35cbca3 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,41 @@ 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
+       }
+
+       if err := validateGlobPattern(pattern); err != nil {
+               return nil, err
+       }
+
+       cmd := fmt.Sprintf("docker exec %s sh -c 'ls -d -- %s 2>/dev/null || 
true'", 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, 
stderr: %s", pattern, err, stderr)
+       }
+
+       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..e3c2527 100644
--- a/internal/components/collector/kind.go
+++ b/internal/components/collector/kind.go
@@ -21,6 +21,7 @@ import (
        "fmt"
        "os"
        "path/filepath"
+       "regexp"
        "strings"
 
        "github.com/apache/skywalking-infra-e2e/internal/config"
@@ -74,8 +75,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 +159,88 @@ func collectPodDescribe(kubeConfigPath, outputDir, 
namespace, podName string) er
        return nil
 }
 
+// containsGlob reports whether the path contains glob metacharacters.
+func containsGlob(path string) bool {
+       if strings.ContainsAny(path, "*?") {
+               return true
+       }
+       // Only treat '[' as a glob when followed by a matching ']' with at 
least
+       // one character between them, so literal brackets (e.g. "app[1].log")
+       // that don't form a valid character class are not misidentified.
+       for i := 0; i < len(path); i++ {
+               if path[i] != '[' {
+                       continue
+               }
+               for j := i + 1; j < len(path); j++ {
+                       if path[j] != ']' {
+                               continue
+                       }
+                       // Check there is at least one non-']' char between '[' 
and ']'.
+                       for k := i + 1; k < j; k++ {
+                               if path[k] != ']' {
+                                       return true
+                               }
+                       }
+                       break
+               }
+       }
+       return false
+}
+
+// validPathPattern matches paths that contain only safe characters for shell 
interpolation.
+// Allowed: alphanumeric, /, ., -, _, *, ?, [, ].
+var validPathPattern = regexp.MustCompile(`^[a-zA-Z0-9/_.*?\[\]\-]+$`)
+
+// validateGlobPattern checks that a glob pattern contains only safe characters
+// to prevent shell injection when interpolated into sh -c commands.
+func validateGlobPattern(pattern string) error {
+       if !validPathPattern.MatchString(pattern) {
+               return fmt.Errorf("glob pattern %q contains unsupported 
characters", pattern)
+       }
+       return nil
+}
+
+// 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
+       }
+
+       if err := validateGlobPattern(pattern); err != nil {
+               return nil, err
+       }
+
+       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 || true'", 
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, 
stderr: %s", pattern, err, stderr)
+       }
+
+       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