github.com/grafana/tanka@v0.26.1-0.20240506093700-c22cfc35c21a/pkg/jsonnet/lint.go (about) 1 package jsonnet 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "time" 10 11 "github.com/gobwas/glob" 12 "github.com/google/go-jsonnet/linter" 13 "github.com/grafana/tanka/pkg/jsonnet/implementations/goimpl" 14 "github.com/grafana/tanka/pkg/jsonnet/jpath" 15 "github.com/pkg/errors" 16 "github.com/rs/zerolog/log" 17 ) 18 19 // LintOpts modifies the behaviour of Lint 20 type LintOpts struct { 21 // Excludes are a list of globs to exclude files while searching for Jsonnet 22 // files 23 Excludes []glob.Glob 24 25 // Parallelism determines the number of workers that will process files 26 Parallelism int 27 28 Out io.Writer 29 } 30 31 // Lint takes a list of files and directories, processes them and prints 32 // out to stderr if there are linting warnings 33 func Lint(fds []string, opts *LintOpts) error { 34 if opts.Parallelism <= 0 { 35 return errors.New("parallelism must be greater than 0") 36 } 37 38 if opts.Out == nil { 39 opts.Out = os.Stdout 40 } 41 42 var paths []string 43 for _, f := range fds { 44 fs, err := FindFiles(f, opts.Excludes) 45 if err != nil { 46 return errors.Wrap(err, "finding Jsonnet files") 47 } 48 paths = append(paths, fs...) 49 } 50 51 type result struct { 52 success bool 53 output string 54 } 55 fileCh := make(chan string, len(paths)) 56 resultCh := make(chan result, len(paths)) 57 lintWorker := func(fileCh <-chan string, resultCh chan result) { 58 for file := range fileCh { 59 buf, success := lintWithRecover(file) 60 resultCh <- result{success: success, output: buf.String()} 61 } 62 } 63 64 for i := 0; i < opts.Parallelism; i++ { 65 go lintWorker(fileCh, resultCh) 66 } 67 68 for _, file := range paths { 69 fileCh <- file 70 } 71 close(fileCh) 72 73 lintingFailed := false 74 for i := 0; i < len(paths); i++ { 75 result := <-resultCh 76 lintingFailed = lintingFailed || !result.success 77 if result.output != "" { 78 fmt.Fprint(opts.Out, result.output) 79 } 80 } 81 82 if lintingFailed { 83 return errors.New("Linting has failed for at least one file") 84 } 85 return nil 86 } 87 88 func lintWithRecover(file string) (buf bytes.Buffer, success bool) { 89 file, err := filepath.Abs(file) 90 if err != nil { 91 fmt.Fprintf(&buf, "got an error getting the absolute path for %s: %v\n\n", file, err) 92 return 93 } 94 95 log.Debug().Str("file", file).Msg("linting file") 96 startTime := time.Now() 97 defer func() { 98 if err := recover(); err != nil { 99 fmt.Fprintf(&buf, "caught a panic while linting %s: %v\n\n", file, err) 100 } 101 log.Debug().Str("file", file).Dur("duration_ms", time.Since(startTime)).Msg("linted file") 102 }() 103 104 content, err := os.ReadFile(file) 105 if err != nil { 106 fmt.Fprintf(&buf, "got an error reading file %s: %v\n\n", file, err) 107 return 108 } 109 110 jpaths, _, _, err := jpath.Resolve(file, true) 111 if err != nil { 112 fmt.Fprintf(&buf, "got an error getting jpath for %s: %v\n\n", file, err) 113 return 114 } 115 vm := goimpl.MakeRawVM(jpaths, nil, nil, 0) 116 117 failed := linter.LintSnippet(vm, &buf, []linter.Snippet{{FileName: file, Code: string(content)}}) 118 return buf, !failed 119 }