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