Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: provide optic-ci with context #81

Merged
merged 1 commit into from
Nov 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions internal/linter/optic/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package optic

// Context provides Optic with external information needed in order to process
// API versioning lifecycle rules. For example, lifecycle rules need to know
// when a change is occurring, and what other versions have deprecated the
// OpenAPI spec version being evaluated.
type Context struct {
// ChangeDate is when the proposed change would occur.
ChangeDate string `json:"changeDate"`

// ChangeResource is the proposed change resource name.
ChangeResource string `json:"changeResource"`

// ChangeVersion is the proposed change version.
ChangeVersion Version `json:"changeVersion"`

// ResourceVersions describes other resource version releases.
ResourceVersions ResourceVersionReleases `json:"resourceVersions,omitempty"`
}

// Version describes an API resource version, a date and a stability.
// Stability is assumed to be GA if not specified.
type Version struct {
Date string `json:"date"`
Stability string `json:"stability,omitempty"`
}

// ResourceVersionReleases describes resource version releases.
type ResourceVersionReleases map[string]VersionStabilityReleases

// VersionStabilityReleases describes version releases.
type VersionStabilityReleases map[string]StabilityReleases

// StabilityReleases describes stability releases.
type StabilityReleases map[string]Release

// Release describes a single resource-version-stability release.
type Release struct {
// DeprecatedBy indicates the other release version that deprecates this
// release.
DeprecatedBy Version `json:"deprecatedBy"`
}
74 changes: 74 additions & 0 deletions internal/linter/optic/linter.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@ package optic
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"time"

"github.com/ghodss/yaml"
"go.uber.org/multierr"

"github.com/snyk/vervet"
"github.com/snyk/vervet/config"
"github.com/snyk/vervet/internal/files"
"github.com/snyk/vervet/internal/linter"
Expand All @@ -26,6 +31,7 @@ type Optic struct {
fromSource files.FileSource
toSource files.FileSource
runner commandRunner
timeNow func() time.Time
}

type commandRunner interface {
Expand Down Expand Up @@ -81,6 +87,7 @@ func New(ctx context.Context, cfg *config.OpticCILinter) (*Optic, error) {
fromSource: fromSource,
toSource: toSource,
runner: &execCommandRunner{},
timeNow: time.Now,
}, nil
}

Expand Down Expand Up @@ -142,6 +149,20 @@ func (o *Optic) runCompare(ctx context.Context, path string) error {
}
var compareArgs, volumeArgs []string

// TODO: This assumes the file being linted is a resource version spec
// file, and not a compiled one. We don't yet have rules that support
// diffing _compiled_ specs; that will require a different context and rule
// set for Vervet Underground integration.
opticCtx, err := o.contextFromPath(path)
if err != nil {
return fmt.Errorf("failed to get context from path %q: %w", path, err)
}
opticCtxJson, err := json.Marshal(&opticCtx)
if err != nil {
return err
}
compareArgs = append(compareArgs, "--context", string(opticCtxJson))

fromFile, err := o.fromSource.Fetch(path)
if err != nil {
return err
Expand Down Expand Up @@ -212,4 +233,57 @@ func (o *Optic) runCompare(ctx context.Context, path string) error {
return nil
}

func (o *Optic) contextFromPath(path string) (*Context, error) {
dateDir := filepath.Dir(path)
resourceDir := filepath.Dir(dateDir)
date, resource := filepath.Base(dateDir), filepath.Base(resourceDir)
if _, err := time.Parse("2006-01-02", date); err != nil {
return nil, err
}
stability, err := o.loadStability(path)
if err != nil {
return nil, err
}
if _, err := vervet.ParseStability(stability); err != nil {
return nil, err
}
return &Context{
ChangeDate: o.timeNow().UTC().Format("2006-01-02"),
ChangeResource: resource,
ChangeVersion: Version{
Date: date,
Stability: stability,
},
}, nil
}

func (o *Optic) loadStability(path string) (string, error) {
var (
doc struct {
Stability string `json:"x-snyk-api-stability"`
}
contentsFile string
err error
)
contentsFile, err = o.fromSource.Fetch(path)
if err != nil {
return "", err
}
if contentsFile == "" {
contentsFile, err = o.toSource.Fetch(path)
if err != nil {
return "", err
}
}
contents, err := ioutil.ReadFile(contentsFile)
if err != nil {
return "", err
}
err = yaml.Unmarshal(contents, &doc)
if err != nil {
return "", err
}
return doc.Stability, nil
}

const cmdTimeout = time.Second * 30
33 changes: 22 additions & 11 deletions internal/linter/optic/linter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os/exec"
"path/filepath"
"testing"
"time"

qt "github.com/frankban/quicktest"
"github.com/go-git/go-git/v5"
Expand Down Expand Up @@ -36,15 +37,21 @@ func TestNewLocalFile(t *testing.T) {
c.Assert(l.fromSource, qt.DeepEquals, files.NilSource{})
c.Assert(l.toSource, qt.DeepEquals, files.LocalFSSource{})

// Set up a local example project
testProject := c.TempDir()
copyFile(c, filepath.Join(testProject, "spec.yaml"), testdata.Path("resources/_examples/hello-world/2021-06-01/spec.yaml"))
versionDir := testProject + "/hello/2021-06-01"
c.Assert(os.MkdirAll(versionDir, 0777), qt.IsNil)
copyFile(c, filepath.Join(versionDir, "spec.yaml"), testdata.Path("resources/_examples/hello-world/2021-06-01/spec.yaml"))
origWd, err := os.Getwd()
c.Assert(err, qt.IsNil)
c.Cleanup(func() { c.Assert(os.Chdir(origWd), qt.IsNil) })
c.Assert(os.Chdir(testProject), qt.IsNil)
cwd, err := os.Getwd()
c.Assert(err, qt.IsNil)

// Mock time for repeatable tests
l.timeNow = func() time.Time { return time.Date(2021, time.October, 30, 1, 2, 3, 0, time.UTC) }

// Capture stdout to a file
tempFile, err := os.Create(c.TempDir() + "/stdout")
c.Assert(err, qt.IsNil)
Expand All @@ -53,16 +60,18 @@ func TestNewLocalFile(t *testing.T) {

runner := &mockRunner{}
l.runner = runner
err = l.Run(ctx, "spec.yaml")
err = l.Run(ctx, "hello/2021-06-01/spec.yaml")
c.Assert(err, qt.IsNil)
c.Assert(runner.runs, qt.DeepEquals, [][]string{{
"docker", "run", "--rm",
"-v", cwd + ":/to",
"-v", cwd + "/spec.yaml:/to/spec.yaml",
"-v", cwd + "/hello/2021-06-01/spec.yaml:/to/hello/2021-06-01/spec.yaml",
"some-image",
"compare",
"--context",
`{"changeDate":"2021-10-30","changeResource":"hello","changeVersion":{"date":"2021-06-01","stability":"experimental"}}`,
"--to",
"/to/spec.yaml",
"/to/hello/2021-06-01/spec.yaml",
}})

// Verify captured output was substituted. Mainly a convenience that makes
Expand All @@ -75,7 +84,7 @@ func TestNewLocalFile(t *testing.T) {
// Command failed.
runner = &mockRunner{err: fmt.Errorf("bad wolf")}
l.runner = runner
err = l.Run(ctx, "spec.yaml")
err = l.Run(ctx, "hello/2021-06-01/spec.yaml")
c.Assert(err, qt.ErrorMatches, ".*: bad wolf")
}

Expand Down Expand Up @@ -129,21 +138,21 @@ func TestNewGitFile(t *testing.T) {
c.Assert(l.toSource, qt.DeepEquals, files.LocalFSSource{})

// Sanity check gitRepoSource
path, err := l.fromSource.Fetch("spec.yaml")
path, err := l.fromSource.Fetch("hello/2021-06-01/spec.yaml")
c.Assert(err, qt.IsNil)
c.Assert(path, qt.Not(qt.Equals), "")

runner := &mockRunner{}
l.runner = runner
err = l.Run(ctx, "spec.yaml")
err = l.Run(ctx, "hello/2021-06-01/spec.yaml")
c.Assert(err, qt.IsNil)
c.Assert(runner.runs[0], qt.Contains, "--from")
c.Assert(runner.runs[0], qt.Contains, "--to")

// Command failed.
runner = &mockRunner{err: fmt.Errorf("bad wolf")}
l.runner = runner
err = l.Run(ctx, "spec.yaml")
err = l.Run(ctx, "hello/2021-06-01/spec.yaml")
c.Assert(err, qt.ErrorMatches, ".*: bad wolf")
}

Expand Down Expand Up @@ -191,10 +200,12 @@ func setupGitRepo(c *qt.C) (string, plumbing.Hash) {
testRepo := c.TempDir()
repo, err := git.PlainInit(testRepo, false)
c.Assert(err, qt.IsNil)
copyFile(c, filepath.Join(testRepo, "spec.yaml"), testdata.Path("resources/_examples/hello-world/2021-06-01/spec.yaml"))
versionDir := testRepo + "/hello/2021-06-01"
c.Assert(os.MkdirAll(versionDir, 0777), qt.IsNil)
copyFile(c, filepath.Join(versionDir, "spec.yaml"), testdata.Path("resources/_examples/hello-world/2021-06-01/spec.yaml"))
worktree, err := repo.Worktree()
c.Assert(err, qt.IsNil)
_, err = worktree.Add("spec.yaml")
_, err = worktree.Add("hello")
c.Assert(err, qt.IsNil)
commitHash, err := worktree.Commit("test: initial commit", &git.CommitOptions{
All: true,
Expand All @@ -204,7 +215,7 @@ func setupGitRepo(c *qt.C) (string, plumbing.Hash) {
},
})
c.Assert(err, qt.IsNil)
copyFile(c, filepath.Join(testRepo, "spec.yaml"), testdata.Path("resources/_examples/hello-world/2021-06-13/spec.yaml"))
copyFile(c, filepath.Join(versionDir, "spec.yaml"), testdata.Path("resources/_examples/hello-world/2021-06-13/spec.yaml"))
return testRepo, commitHash
}

Expand Down
2 changes: 2 additions & 0 deletions version.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ func ParseStability(s string) (Stability, error) {
return StabilityExperimental, nil
case "beta":
return StabilityBeta, nil
case "ga":
return StabilityGA, nil
default:
return stabilityUndefined, fmt.Errorf("invalid stability %q", s)
}
Expand Down