Skip to content

Commit f2c8e59

Browse files
authored
Merge pull request #92 from cmars/feat/optic-bulk-compare
feat: support optic bulk compare
2 parents 42c3c9b + 9f233df commit f2c8e59

File tree

4 files changed

+88
-36
lines changed

4 files changed

+88
-36
lines changed

internal/files/files.go

+9
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import (
1414
// FileSource defines a source of spec files to lint. This abstraction allows
1515
// linters to operate seamlessly over version control systems and local files.
1616
type FileSource interface {
17+
// Name returns a string describing the file source.
18+
Name() string
19+
1720
// Match returns a slice of logical paths to spec files that should be
1821
// linted from the given resource set configuration.
1922
Match(*config.ResourceSet) ([]string, error)
@@ -32,6 +35,9 @@ type FileSource interface {
3235
// NilSource is a FileSource that does not have any files in it.
3336
type NilSource struct{}
3437

38+
// Name implements FileSource.
39+
func (NilSource) Name() string { return "does not exist" }
40+
3541
// Match implements FileSource.
3642
func (NilSource) Match(*config.ResourceSet) ([]string, error) { return nil, nil }
3743

@@ -47,6 +53,9 @@ func (NilSource) Close() error { return nil }
4753
// relative to the current working directory.
4854
type LocalFSSource struct{}
4955

56+
// Name implements FileSource.
57+
func (LocalFSSource) Name() string { return "local file" }
58+
5059
// Match implements FileSource.
5160
func (LocalFSSource) Match(rcConfig *config.ResourceSet) ([]string, error) {
5261
var result []string

internal/linter/optic/git.go

+5
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ func newGitRepoSource(path string, treeish string) (*gitRepoSource, error) {
4444
return &gitRepoSource{repo: repo, commit: commit, tempDir: tempDir}, nil
4545
}
4646

47+
// Name implements FileSource.
48+
func (s *gitRepoSource) Name() string {
49+
return "commit " + s.commit.Hash.String()
50+
}
51+
4752
// Match implements FileSource.
4853
func (s *gitRepoSource) Match(rcConfig *config.ResourceSet) ([]string, error) {
4954
tree, err := s.repo.TreeObject(s.commit.TreeHash)

internal/linter/optic/linter.go

+58-20
Original file line numberDiff line numberDiff line change
@@ -131,44 +131,58 @@ func (*Optic) WithOverride(ctx context.Context, override *config.Linter) (linter
131131
// output by Optic CI. Returns an error when lint fails configured rules.
132132
func (o *Optic) Run(ctx context.Context, paths ...string) error {
133133
var errs error
134+
var comparisons []comparison
135+
var dockerArgs []string
134136
for i := range paths {
135-
err := o.runCompare(ctx, paths[i])
137+
comparison, volumeArgs, err := o.newComparison(paths[i])
136138
if err != nil {
137139
errs = multierr.Append(errs, err)
140+
} else {
141+
comparisons = append(comparisons, comparison)
142+
dockerArgs = append(dockerArgs, volumeArgs...)
138143
}
139144
}
145+
err := o.bulkCompare(ctx, comparisons, dockerArgs)
146+
errs = multierr.Append(errs, err)
140147
return errs
141148
}
142149

143-
var opticOutputRE = regexp.MustCompile(`/(from|to)`)
150+
type comparison struct {
151+
From string `json:"from"`
152+
To string `json:"to"`
153+
Context Context `json:"context"`
154+
}
155+
156+
type bulkCompareInput struct {
157+
Comparisons []comparison `json:"comparisons"`
158+
}
144159

145-
func (o *Optic) runCompare(ctx context.Context, path string) error {
160+
func (o *Optic) newComparison(path string) (comparison, []string, error) {
146161
cwd, err := os.Getwd()
147162
if err != nil {
148-
return err
163+
return comparison{}, nil, err
149164
}
150-
var compareArgs, volumeArgs []string
165+
var volumeArgs []string
151166

152167
// TODO: This assumes the file being linted is a resource version spec
153168
// file, and not a compiled one. We don't yet have rules that support
154169
// diffing _compiled_ specs; that will require a different context and rule
155170
// set for Vervet Underground integration.
156171
opticCtx, err := o.contextFromPath(path)
157172
if err != nil {
158-
return fmt.Errorf("failed to get context from path %q: %w", path, err)
173+
return comparison{}, nil, fmt.Errorf("failed to get context from path %q: %w", path, err)
159174
}
160-
opticCtxJson, err := json.Marshal(&opticCtx)
161-
if err != nil {
162-
return err
175+
176+
cmp := comparison{
177+
Context: *opticCtx,
163178
}
164-
compareArgs = append(compareArgs, "--context", string(opticCtxJson))
165179

166180
fromFile, err := o.fromSource.Fetch(path)
167181
if err != nil {
168-
return err
182+
return comparison{}, nil, err
169183
}
170184
if fromFile != "" {
171-
compareArgs = append(compareArgs, "--from", "/from/"+path)
185+
cmp.From = "/from/" + path
172186
volumeArgs = append(volumeArgs,
173187
"-v", cwd+":/from",
174188
"-v", fromFile+":/from/"+path,
@@ -177,21 +191,42 @@ func (o *Optic) runCompare(ctx context.Context, path string) error {
177191

178192
toFile, err := o.toSource.Fetch(path)
179193
if err != nil {
180-
return err
194+
return comparison{}, nil, err
181195
}
182196
if toFile != "" {
183-
compareArgs = append(compareArgs, "--to", "/to/"+path)
197+
cmp.To = "/to/" + path
184198
volumeArgs = append(volumeArgs,
185199
"-v", cwd+":/to",
186200
"-v", toFile+":/to/"+path,
187201
)
188202
}
189203

190-
// TODO: provide context JSON object in --context
204+
return cmp, volumeArgs, nil
205+
}
206+
207+
var opticFromOutputRE = regexp.MustCompile(`/from/`)
208+
var opticToOutputRE = regexp.MustCompile(`/to/`)
209+
210+
func (o *Optic) bulkCompare(ctx context.Context, comparisons []comparison, dockerArgs []string) error {
211+
input := &bulkCompareInput{
212+
Comparisons: comparisons,
213+
}
214+
inputFile, err := ioutil.TempFile("", "*-input.json")
215+
if err != nil {
216+
return err
217+
}
218+
defer inputFile.Close()
219+
err = json.NewEncoder(inputFile).Encode(&input)
220+
if err != nil {
221+
return err
222+
}
223+
if err := inputFile.Sync(); err != nil {
224+
return err
225+
}
226+
191227
// TODO: link to command line arguments for optic-ci when available.
192-
cmdline := append([]string{"run", "--rm"}, volumeArgs...)
193-
cmdline = append(cmdline, o.image, "compare")
194-
cmdline = append(cmdline, compareArgs...)
228+
cmdline := append([]string{"run", "--rm", "-v", inputFile.Name() + ":/input.json"}, dockerArgs...)
229+
cmdline = append(cmdline, o.image, "bulk-compare", "--input", "/input.json")
195230
log.Println(cmdline)
196231
cmd := exec.CommandContext(ctx, "docker", cmdline...)
197232

@@ -216,7 +251,10 @@ func (o *Optic) runCompare(ctx context.Context, path string) error {
216251
defer pipeReader.Close()
217252
sc := bufio.NewScanner(pipeReader)
218253
for sc.Scan() {
219-
fmt.Println(opticOutputRE.ReplaceAllLiteralString(sc.Text(), cwd))
254+
line := sc.Text()
255+
line = opticFromOutputRE.ReplaceAllString(line, "("+o.fromSource.Name()+"):")
256+
line = opticToOutputRE.ReplaceAllString(line, "("+o.toSource.Name()+"):")
257+
fmt.Println(line)
220258
}
221259
if err := sc.Err(); err != nil {
222260
fmt.Fprintf(os.Stderr, "error reading stdout: %v", err)
@@ -228,7 +266,7 @@ func (o *Optic) runCompare(ctx context.Context, path string) error {
228266
cmd.Stderr = os.Stderr
229267
err = o.runner.run(cmd)
230268
if err != nil {
231-
return fmt.Errorf("lint %q failed: %w", path, err)
269+
return fmt.Errorf("lint failed: %w", err)
232270
}
233271
return nil
234272
}

internal/linter/optic/linter_test.go

+16-16
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"os/exec"
99
"path/filepath"
10+
"strings"
1011
"testing"
1112
"time"
1213

@@ -46,8 +47,6 @@ func TestNewLocalFile(t *testing.T) {
4647
c.Assert(err, qt.IsNil)
4748
c.Cleanup(func() { c.Assert(os.Chdir(origWd), qt.IsNil) })
4849
c.Assert(os.Chdir(testProject), qt.IsNil)
49-
cwd, err := os.Getwd()
50-
c.Assert(err, qt.IsNil)
5150

5251
// Mock time for repeatable tests
5352
l.timeNow = func() time.Time { return time.Date(2021, time.October, 30, 1, 2, 3, 0, time.UTC) }
@@ -62,24 +61,18 @@ func TestNewLocalFile(t *testing.T) {
6261
l.runner = runner
6362
err = l.Run(ctx, "hello/2021-06-01/spec.yaml")
6463
c.Assert(err, qt.IsNil)
65-
c.Assert(runner.runs, qt.DeepEquals, [][]string{{
66-
"docker", "run", "--rm",
67-
"-v", cwd + ":/to",
68-
"-v", cwd + "/hello/2021-06-01/spec.yaml:/to/hello/2021-06-01/spec.yaml",
69-
"some-image",
70-
"compare",
71-
"--context",
72-
`{"changeDate":"2021-10-30","changeResource":"hello","changeVersion":{"date":"2021-06-01","stability":"experimental"}}`,
73-
"--to",
74-
"/to/hello/2021-06-01/spec.yaml",
75-
}})
64+
c.Assert(runner.runs, qt.HasLen, 1)
65+
c.Assert(strings.Join(runner.runs[0], " "), qt.Matches,
66+
``+
67+
`^docker run --rm -v .*:/input.json -v .*:/to -v .*/hello/2021-06-01/spec.yaml:/to/hello/2021-06-01/spec.yaml `+
68+
`some-image bulk-compare --input /input.json`)
7669

7770
// Verify captured output was substituted. Mainly a convenience that makes
7871
// output host-relevant and cmd-clickable if possible.
7972
c.Assert(tempFile.Sync(), qt.IsNil)
8073
capturedOutput, err := ioutil.ReadFile(tempFile.Name())
8174
c.Assert(err, qt.IsNil)
82-
c.Assert(string(capturedOutput), qt.Equals, cwd+"/here.yaml "+cwd+"/eternity.yaml\n")
75+
c.Assert(string(capturedOutput), qt.Equals, "(does not exist):here.yaml (local file):eternity.yaml\n")
8376

8477
// Command failed.
8578
runner = &mockRunner{err: fmt.Errorf("bad wolf")}
@@ -146,8 +139,11 @@ func TestNewGitFile(t *testing.T) {
146139
l.runner = runner
147140
err = l.Run(ctx, "hello/2021-06-01/spec.yaml")
148141
c.Assert(err, qt.IsNil)
149-
c.Assert(runner.runs[0], qt.Contains, "--from")
150-
c.Assert(runner.runs[0], qt.Contains, "--to")
142+
c.Assert(runner.runs, qt.HasLen, 1)
143+
c.Assert(strings.Join(runner.runs[0], " "), qt.Matches,
144+
``+
145+
`^docker run --rm -v .*:/input.json -v .*:/to -v .*/hello/2021-06-01/spec.yaml:/to/hello/2021-06-01/spec.yaml `+
146+
`some-image bulk-compare --input /input.json`)
151147

152148
// Command failed.
153149
runner = &mockRunner{err: fmt.Errorf("bad wolf")}
@@ -221,6 +217,10 @@ func setupGitRepo(c *qt.C) (string, plumbing.Hash) {
221217

222218
type mockSource []string
223219

220+
func (m mockSource) Name() string {
221+
return "mock"
222+
}
223+
224224
func (m mockSource) Match(*config.ResourceSet) ([]string, error) {
225225
return m, nil
226226
}

0 commit comments

Comments
 (0)