github.com/snyk/vervet/v3@v3.7.0/internal/linter/sweatercomb/linter.go (about) 1 package sweatercomb 2 3 import ( 4 "bufio" 5 "context" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "log" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "regexp" 14 "time" 15 16 "github.com/ghodss/yaml" 17 18 "github.com/snyk/vervet/v3/config" 19 "github.com/snyk/vervet/v3/internal/files" 20 "github.com/snyk/vervet/v3/internal/linter" 21 ) 22 23 // SweaterComb runs a Docker image containing Spectral and some built-in rules, 24 // along with additional user-specified rules. 25 type SweaterComb struct { 26 image string 27 rules []string 28 extraArgs []string 29 30 rulesDir string 31 32 runner commandRunner 33 } 34 35 type commandRunner interface { 36 run(cmd *exec.Cmd) error 37 } 38 39 type execCommandRunner struct{} 40 41 func (*execCommandRunner) run(cmd *exec.Cmd) error { 42 return cmd.Run() 43 } 44 45 // New returns a new SweaterComb instance configured with the given rules. 46 func New(ctx context.Context, cfg *config.SweaterCombLinter) (*SweaterComb, error) { 47 image, rules, extraArgs := cfg.Image, cfg.Rules, cfg.ExtraArgs 48 if len(rules) == 0 { 49 return nil, fmt.Errorf("missing spectral rules") 50 } 51 52 rulesDir, err := ioutil.TempDir("", "*-scrules") 53 if err != nil { 54 return nil, fmt.Errorf("failed to create temp rules directory: %w", err) 55 } 56 rulesFile, err := os.Create(filepath.Join(rulesDir, "ruleset.yaml")) 57 if err != nil { 58 return nil, fmt.Errorf("failed to create temp rules file: %w", err) 59 } 60 defer rulesFile.Close() 61 resolvedRules := make([]string, len(rules)) 62 for i := range rules { 63 rule := filepath.Clean(rules[i]) 64 if !filepath.IsAbs(rule) { 65 rule = "/sweater-comb/target/" + rule 66 } 67 resolvedRules[i] = rule 68 if err != nil { 69 return nil, err 70 } 71 } 72 rulesDoc := map[string]interface{}{ 73 "extends": resolvedRules, 74 } 75 rulesBuf, err := yaml.Marshal(&rulesDoc) 76 if err != nil { 77 return nil, fmt.Errorf("failed to marshal temp rules file: %w", err) 78 } 79 _, err = rulesFile.Write(rulesBuf) 80 if err != nil { 81 return nil, fmt.Errorf("failed to marshal temp rules file: %w", err) 82 } 83 go func() { 84 <-ctx.Done() 85 os.RemoveAll(rulesDir) 86 }() 87 return &SweaterComb{ 88 image: image, 89 rules: resolvedRules, 90 rulesDir: rulesDir, 91 extraArgs: extraArgs, 92 runner: &execCommandRunner{}, 93 }, nil 94 } 95 96 // Match implements linter.Linter. 97 func (s *SweaterComb) Match(rcConfig *config.ResourceSet) ([]string, error) { 98 return files.LocalFSSource{}.Match(rcConfig) 99 } 100 101 // WithOverride implements linter.Linter. 102 func (s *SweaterComb) WithOverride(ctx context.Context, override *config.Linter) (linter.Linter, error) { 103 if override.SweaterComb == nil { 104 return nil, fmt.Errorf("invalid linter override") 105 } 106 merged := *override.SweaterComb 107 merged.Rules = append(s.rules, merged.Rules...) 108 return New(ctx, &merged) 109 } 110 111 var sweaterCombOutputRE = regexp.MustCompile(`/sweater-comb/target`) 112 113 // Run runs spectral on the given paths. Linting output is written to standard 114 // output by spectral. Returns an error when lint fails configured rules. 115 func (s *SweaterComb) Run(ctx context.Context, _ string, paths ...string) error { 116 cwd, err := os.Getwd() 117 if err != nil { 118 return err 119 } 120 mountedPaths := make([]string, len(paths)) 121 for i := range paths { 122 mountedPaths[i] = filepath.Join("./", paths[i]) 123 } 124 cmdline := append(append([]string{ 125 "run", "--rm", 126 "-v", s.rulesDir + ":/vervet", "-v", cwd + ":/sweater-comb/target", 127 s.image, 128 "lint", 129 "-r", "/vervet/ruleset.yaml", 130 }, s.extraArgs...), paths...) 131 cmd := exec.CommandContext(ctx, "docker", cmdline...) 132 133 pipeReader, pipeWriter := io.Pipe() 134 ch := make(chan struct{}) 135 defer func() { 136 err := pipeWriter.Close() 137 if err != nil { 138 log.Printf("warning: failed to close output: %v", err) 139 } 140 select { 141 case <-ch: 142 case <-ctx.Done(): 143 case <-time.After(cmdTimeout): 144 log.Printf("warning: timeout waiting for output to flush") 145 } 146 }() 147 go func() { 148 defer pipeReader.Close() 149 sc := bufio.NewScanner(pipeReader) 150 for sc.Scan() { 151 fmt.Println(sweaterCombOutputRE.ReplaceAllLiteralString(sc.Text(), cwd)) 152 } 153 if err := sc.Err(); err != nil { 154 fmt.Fprintf(os.Stderr, "error reading stdout: %v", err) 155 } 156 close(ch) 157 }() 158 cmd.Stdin = os.Stdin 159 cmd.Stdout = pipeWriter 160 cmd.Stderr = os.Stderr 161 return s.runner.run(cmd) 162 } 163 164 const cmdTimeout = time.Second * 10