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  }