github.com/kubeshop/testkube@v1.17.23/pkg/tcl/expressionstcl/libs/fs.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 libs 10 11 import ( 12 "errors" 13 "fmt" 14 "io" 15 "io/fs" 16 "path/filepath" 17 "strings" 18 19 "github.com/bmatcuk/doublestar/v4" 20 21 "github.com/kubeshop/testkube/pkg/tcl/expressionstcl" 22 ) 23 24 func absPath(p, workingDir string) string { 25 if !filepath.IsAbs(p) { 26 p = filepath.Join(workingDir, p) 27 } 28 v, err := filepath.Abs(p) 29 if err != nil { 30 v = p 31 } 32 v = strings.TrimRight(filepath.ToSlash(v), "/") 33 if v == "" { 34 return "/" 35 } 36 return v 37 } 38 39 func findSearchRoot(pattern, workingDir string) string { 40 path, _ := doublestar.SplitPattern(pattern + "/") 41 path = strings.TrimRight(path, "/") 42 if path == "." { 43 return strings.TrimLeft(absPath("", workingDir), "/") 44 } 45 return strings.TrimLeft(path, "/") 46 } 47 48 func mapSlice[T any, U any](s []T, fn func(T) U) []U { 49 result := make([]U, len(s)) 50 for i := range s { 51 result[i] = fn(s[i]) 52 } 53 return result 54 } 55 56 func deduplicateRoots(paths []string) []string { 57 result := make([]string, 0) 58 unique := make(map[string]struct{}) 59 for _, p := range paths { 60 unique[p] = struct{}{} 61 } 62 loop: 63 for path := range unique { 64 for path2 := range unique { 65 if strings.HasPrefix(path, path2+"/") { 66 continue loop 67 } 68 } 69 result = append(result, path) 70 } 71 return result 72 } 73 74 func readFile(fsys fs.FS, workingDir string, values ...expressionstcl.StaticValue) (interface{}, error) { 75 if len(values) != 1 { 76 return nil, errors.New("file() function takes a single argument") 77 } 78 if !values[0].IsString() { 79 return nil, fmt.Errorf("file() function expects a string argument, provided: %v", values[0].String()) 80 } 81 filePath, _ := values[0].StringValue() 82 file, err := fsys.Open(strings.TrimLeft(absPath(filePath, workingDir), "/")) 83 if err != nil { 84 return nil, fmt.Errorf("opening file(%s): %s", filePath, err.Error()) 85 } 86 content, err := io.ReadAll(file) 87 if err != nil { 88 return nil, fmt.Errorf("reading file(%s): %s", filePath, err.Error()) 89 } 90 return string(content), nil 91 } 92 93 func createGlobMatcher(patterns []string) func(string) bool { 94 return func(filePath string) bool { 95 for _, p := range patterns { 96 v, _ := doublestar.PathMatch(p, filePath) 97 if v { 98 return true 99 } 100 } 101 return false 102 } 103 } 104 105 func globFs(fsys fs.FS, workingDir string, values ...expressionstcl.StaticValue) (interface{}, error) { 106 if len(values) == 0 { 107 return nil, errors.New("glob() function takes at least one argument") 108 } 109 110 // Read all the patterns 111 ignorePatterns := make([]string, 0) 112 patterns := make([]string, 0) 113 for i := 0; i < len(values); i++ { 114 v, _ := values[i].StringValue() 115 if strings.HasPrefix(v, "!") { 116 ignorePatterns = append(ignorePatterns, absPath(v[1:], workingDir)) 117 } else { 118 patterns = append(patterns, absPath(v, workingDir)) 119 } 120 } 121 if len(patterns) == 0 { 122 return nil, errors.New("glob() function needs at least one matching pattern") 123 } 124 matchesPositive := createGlobMatcher(patterns) 125 matchesIgnore := createGlobMatcher(ignorePatterns) 126 127 // Determine roots for searching, to avoid scanning whole FS 128 findRoot := func(pattern string) string { 129 return findSearchRoot(pattern, workingDir) 130 } 131 roots := deduplicateRoots(mapSlice(patterns, findRoot)) 132 result := make([]string, 0) 133 for _, root := range roots { 134 err := fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error { 135 if path == "." || err != nil || d.IsDir() { 136 return nil 137 } 138 path = "/" + path 139 if !matchesPositive(path) || matchesIgnore(path) { 140 return nil 141 } 142 result = append(result, path) 143 return nil 144 }) 145 if err != nil { 146 return nil, fmt.Errorf("glob() error: %v", err) 147 } 148 } 149 150 return result, nil 151 } 152 153 func NewFsMachine(fsys fs.FS, workingDir string) expressionstcl.Machine { 154 if workingDir == "" { 155 workingDir = "/" 156 } 157 return expressionstcl.NewMachine(). 158 RegisterFunction("file", func(values ...expressionstcl.StaticValue) (interface{}, bool, error) { 159 v, err := readFile(fsys, workingDir, values...) 160 return v, true, err 161 }). 162 RegisterFunction("glob", func(values ...expressionstcl.StaticValue) (interface{}, bool, error) { 163 v, err := globFs(fsys, workingDir, values...) 164 return v, true, err 165 }) 166 }