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/
