github.com/kubeshop/testkube@v1.17.23/cmd/tcl/testworkflow-toolkit/artifacts/walker.go (about) 1 // Copyright 2024 Testkube. 2 // 3 // Licensed as a Testkube Pro file under the Testkube Community 4 // License (the "License"); you may not use this file except in compliance with 5 // the License. You may obtain a copy of the License at 6 // 7 // https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt 8 9 package artifacts 10 11 import ( 12 "fmt" 13 "io/fs" 14 "path/filepath" 15 "strings" 16 17 "github.com/bmatcuk/doublestar/v4" 18 ) 19 20 func mapSlice[T any, U any](s []T, fn func(T) U) []U { 21 result := make([]U, len(s)) 22 for i := range s { 23 result[i] = fn(s[i]) 24 } 25 return result 26 } 27 28 func deduplicateRoots(paths []string) []string { 29 result := make([]string, 0) 30 unique := make(map[string]struct{}) 31 for _, p := range paths { 32 unique[p] = struct{}{} 33 } 34 loop: 35 for path := range unique { 36 for path2 := range unique { 37 if strings.HasPrefix(path, path2+"/") { 38 continue loop 39 } 40 } 41 result = append(result, path) 42 } 43 return result 44 } 45 46 func findSearchRoot(pattern string) string { 47 path, _ := doublestar.SplitPattern(pattern + "/") 48 return strings.TrimRight(path, "/") 49 } 50 51 // TODO: Support wildcards better: 52 // - /**/*.json is a part of /data 53 // - /data/s*me/*a*/abc.json is a part of /data/some/path/ 54 func isPatternIn(pattern string, dirs []string) bool { 55 return isPathIn(findSearchRoot(pattern), dirs) 56 } 57 58 func isPathIn(path string, dirs []string) bool { 59 for _, dir := range dirs { 60 path = strings.TrimRight(path, "/") 61 if dir == path || strings.HasPrefix(path, dir+"/") { 62 return true 63 } 64 } 65 return false 66 } 67 68 func sanitizePath(path string) (string, error) { 69 path, err := filepath.Abs(path) 70 path = strings.TrimRight(filepath.ToSlash(path), "/") 71 if path == "" { 72 path = "/" 73 } 74 return path, err 75 } 76 77 func sanitizePaths(input []string) ([]string, error) { 78 paths := make([]string, len(input)) 79 for i := range input { 80 var err error 81 paths[i], err = sanitizePath(input[i]) 82 if err != nil { 83 return nil, fmt.Errorf("error while resolving path: %s: %w", input[i], err) 84 } 85 } 86 return paths, nil 87 } 88 89 func filterPatterns(patterns, dirs []string) []string { 90 result := make([]string, 0) 91 for _, p := range patterns { 92 if isPatternIn(p, dirs) { 93 result = append(result, p) 94 } 95 } 96 return result 97 } 98 99 func detectCommonPath(path1, path2 string) string { 100 if path1 == path2 { 101 return path1 102 } 103 common := 0 104 parts1 := strings.Split(path1, "/") 105 parts2 := strings.Split(path2, "/") 106 for i := 0; i < len(parts1) && i < len(parts2); i++ { 107 if parts1[i] != parts2[i] { 108 break 109 } 110 common++ 111 } 112 if common == 1 && parts1[0] == "" { 113 return "/" 114 } 115 return strings.Join(parts1[0:common], "/") 116 } 117 118 func detectRoot(potential string, paths []string) string { 119 potential = strings.TrimRight(potential, "/") 120 if potential == "" { 121 potential = "/" 122 } 123 for _, path := range paths { 124 potential = detectCommonPath(potential, path) 125 } 126 return potential 127 } 128 129 func CreateWalker(patterns, roots []string, root string) (Walker, error) { 130 var err error 131 132 // Build absolute paths 133 if patterns, err = sanitizePaths(patterns); err != nil { 134 return nil, err 135 } 136 if roots, err = sanitizePaths(roots); err != nil { 137 return nil, err 138 } 139 if root, err = sanitizePath(root); err != nil { 140 return nil, err 141 } 142 // Include only if it is matching some mounted volumes 143 patterns = filterPatterns(patterns, roots) 144 // Detect top-level paths for searching 145 searchPaths := deduplicateRoots(mapSlice(patterns, findSearchRoot)) 146 // Detect root path for the bucket 147 root = detectRoot(root, searchPaths) 148 149 return &walker{ 150 root: root, 151 searchPaths: searchPaths, 152 patterns: patterns, 153 }, nil 154 } 155 156 type walker struct { 157 root string 158 searchPaths []string 159 patterns []string // TODO: Optimize to check only patterns matching specific searchPaths 160 } 161 162 type WalkerFn = func(path string, file fs.File, err error) error 163 164 type Walker interface { 165 Root() string 166 SearchPaths() []string 167 Patterns() []string 168 Walk(fsys fs.FS, walker WalkerFn) error 169 } 170 171 func (w *walker) Root() string { 172 return w.root 173 } 174 175 func (w *walker) SearchPaths() []string { 176 return w.searchPaths 177 } 178 179 func (w *walker) Patterns() []string { 180 return w.patterns 181 } 182 183 // TODO: Support negative patterns 184 func (w *walker) matches(filePath string) bool { 185 for _, p := range w.patterns { 186 v, _ := doublestar.PathMatch(p, filePath) 187 if v { 188 return true 189 } 190 } 191 return false 192 } 193 194 func (w *walker) walk(fsys fs.FS, path string, walker WalkerFn) error { 195 sanitizedPath := strings.TrimLeft(path, "/") 196 if sanitizedPath == "" { 197 sanitizedPath = "." 198 } 199 200 return fs.WalkDir(fsys, sanitizedPath, func(filePath string, d fs.DirEntry, err error) error { 201 resolvedPath := "/" + filepath.ToSlash(filePath) 202 if !w.matches(resolvedPath) { 203 return nil 204 } 205 if err != nil { 206 fmt.Printf("Warning: '%s' ignored from scraping: %v\n", resolvedPath, err) 207 return nil 208 } 209 if d.IsDir() { 210 return nil 211 } 212 213 file, err := fsys.Open(filePath) 214 return walker(strings.TrimLeft(resolvedPath[len(w.root):], "/"), file, err) 215 }) 216 } 217 218 func (w *walker) Walk(fsys fs.FS, walker WalkerFn) (err error) { 219 for _, s := range w.searchPaths { 220 err = w.walk(fsys, s, walker) 221 if err != nil { 222 return err 223 } 224 } 225 return nil 226 }