Skip to content

Commit

Permalink
Verify fastly crate version during compute build. (#67)
Browse files Browse the repository at this point in the history
* Verify fastly crate version is up-to-date during compute build.

* Fix typo

Co-authored-by: Adam C. Foltzer <acfoltzer@fastly.com>

* Change fastly crate update remediation error message.

* Add more logic crate update messaging to cover edge cases.

* Add more robust tests for crate verification logic

* Apply suggestions from code review

Co-authored-by: Peter Bourgon <peterbourgon@users.noreply.github.com>

Co-authored-by: Adam C. Foltzer <acfoltzer@fastly.com>
Co-authored-by: Peter Bourgon <peterbourgon@users.noreply.github.com>
  • Loading branch information
3 people authored May 12, 2020
1 parent aed8b68 commit d5de911
Show file tree
Hide file tree
Showing 7 changed files with 341 additions and 44 deletions.
2 changes: 1 addition & 1 deletion pkg/app/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func Run(args []string, env config.Environment, file config.File, configFilePath

computeRoot := compute.NewRootCommand(app, &globals)
computeInit := compute.NewInitCommand(computeRoot.CmdClause, &globals)
computeBuild := compute.NewBuildCommand(computeRoot.CmdClause, &globals)
computeBuild := compute.NewBuildCommand(computeRoot.CmdClause, httpClient, &globals)
computeDeploy := compute.NewDeployCommand(computeRoot.CmdClause, httpClient, &globals)
computeUpdate := compute.NewUpdateCommand(computeRoot.CmdClause, httpClient, &globals)
computeValidate := compute.NewValidateCommand(computeRoot.CmdClause, &globals)
Expand Down
28 changes: 12 additions & 16 deletions pkg/compute/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"path/filepath"
"strings"

"github.com/fastly/cli/pkg/api"
"github.com/fastly/cli/pkg/common"
"github.com/fastly/cli/pkg/compute/manifest"
"github.com/fastly/cli/pkg/config"
Expand All @@ -20,16 +21,6 @@ type Toolchain interface {
Build(out io.Writer, verbose bool) error
}

// GetToolchain returns a Toolchain for the provided language.
func GetToolchain(l string) (Toolchain, error) {
switch l {
case "rust":
return &Rust{}, nil
default:
return nil, fmt.Errorf("unsupported language %s", l)
}
}

func createPackageArchive(files []string, destination string) error {
tar := archiver.NewTarGz()
tar.OverwriteExisting = true //
Expand All @@ -47,14 +38,16 @@ func createPackageArchive(files []string, destination string) error {
// BuildCommand produces a deployable artifact from files on the local disk.
type BuildCommand struct {
common.Base
name string
lang string
client api.HTTPClient
name string
lang string
}

// NewBuildCommand returns a usable command registered under the parent.
func NewBuildCommand(parent common.Registerer, globals *config.Data) *BuildCommand {
func NewBuildCommand(parent common.Registerer, client api.HTTPClient, globals *config.Data) *BuildCommand {
var c BuildCommand
c.Globals = globals
c.client = client
c.CmdClause = parent.Command("build", "Build a Compute@Edge package locally")
c.CmdClause.Flag("name", "Package name").StringVar(&c.name)
c.CmdClause.Flag("language", "Language type").StringVar(&c.lang)
Expand Down Expand Up @@ -108,9 +101,12 @@ func (c *BuildCommand) Exec(in io.Reader, out io.Writer) (err error) {
}
name = sanitize.BaseName(name)

toolchain, err := GetToolchain(lang)
if err != nil {
return err
var toolchain Toolchain
switch lang {
case "rust":
toolchain = &Rust{c.client}
default:
return fmt.Errorf("unsupported language %s", lang)
}

progress.Step(fmt.Sprintf("Verifying local %s toolchain...", lang))
Expand Down
155 changes: 132 additions & 23 deletions pkg/compute/compute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"

"github.com/fastly/cli/pkg/api"
Expand Down Expand Up @@ -274,39 +275,75 @@ func TestBuild(t *testing.T) {
}

for _, testcase := range []struct {
name string
args []string
manifest string
wantError string
wantOutputContains string
name string
args []string
fastlyManifest string
cargoManifest string
cargoLock string
client api.HTTPClient
wantError string
wantRemediationError string
wantOutputContains string
}{
{
name: "no fastly.toml manifest",
args: []string{"compute", "build"},
client: versionClient{[]string{"0.0.0"}},
wantError: "error reading package manifest: open fastly.toml:", // actual message differs on Windows
},
{
name: "empty language",
args: []string{"compute", "build"},
manifest: "name = \"test\"\n",
wantError: "language cannot be empty, please provide a language",
name: "empty language",
args: []string{"compute", "build"},
fastlyManifest: "name = \"test\"\n",
client: versionClient{[]string{"0.0.0"}},
wantError: "language cannot be empty, please provide a language",
},
{
name: "empty name",
args: []string{"compute", "build"},
manifest: "language = \"rust\"\n",
wantError: "name cannot be empty, please provide a name",
name: "empty name",
args: []string{"compute", "build"},
fastlyManifest: "language = \"rust\"\n",
client: versionClient{[]string{"0.0.0"}},
wantError: "name cannot be empty, please provide a name",
},
{
name: "unknown language",
args: []string{"compute", "build"},
manifest: "name = \"test\"\nlanguage = \"javascript\"\n",
wantError: "unsupported language javascript",
name: "unknown language",
args: []string{"compute", "build"},
fastlyManifest: "name = \"test\"\nlanguage = \"javascript\"\n",
client: versionClient{[]string{"0.0.0"}},
wantError: "unsupported language javascript",
},
{
name: "fastly crate not found",
args: []string{"compute", "build"},
fastlyManifest: "name = \"test\"\nlanguage = \"rust\"\n",
cargoLock: " ",
client: versionClient{[]string{"0.4.0"}},
wantError: "fastly crate not found",
wantRemediationError: "fastly = \"^0.4.0\"",
},
{
name: "fastly crate out-of-date but within minor range",
args: []string{"compute", "build"},
fastlyManifest: "name = \"test\"\nlanguage = \"rust\"\n",
cargoLock: "[[package]]\nname = \"fastly\"\nversion = \"0.3.2\"",
client: versionClient{[]string{"0.3.3"}},
wantError: "fastly crate not up-to-date",
wantRemediationError: "cargo update -p fastly",
},
{
name: "fastly crate out-of-date but lower than minor range",
args: []string{"compute", "build"},
fastlyManifest: "name = \"test\"\nlanguage = \"rust\"\n",
cargoLock: "[[package]]\nname = \"fastly\"\nversion = \"0.3.2\"",
client: versionClient{[]string{"0.4.0"}},
wantError: "fastly crate not up-to-date",
wantRemediationError: "fastly = \"^0.4.0\"",
},
{
name: "success",
args: []string{"compute", "build"},
manifest: "name = \"test\"\nlanguage = \"rust\"\n",
fastlyManifest: "name = \"test\"\nlanguage = \"rust\"\n",
client: versionClient{[]string{"0.0.0"}},
wantOutputContains: "Built rust package test",
},
} {
Expand All @@ -320,7 +357,7 @@ func TestBuild(t *testing.T) {

// Create our build environment in a temp dir.
// Defer a call to clean it up.
rootdir := makeBuildEnvironment(t, testcase.manifest)
rootdir := makeBuildEnvironment(t, testcase.fastlyManifest, testcase.cargoManifest, testcase.cargoLock)
defer os.RemoveAll(rootdir)

// Before running the test, chdir into the build environment.
Expand All @@ -337,14 +374,15 @@ func TestBuild(t *testing.T) {
file = config.File{}
appConfigFile = "/dev/null"
clientFactory = mock.APIClient(mock.API{})
httpClient = http.DefaultClient
httpClient = testcase.client
versioner update.Versioner = nil
in io.Reader = nil
buf bytes.Buffer
out io.Writer = common.NewSyncWriter(&buf)
)
err = app.Run(args, env, file, appConfigFile, clientFactory, httpClient, versioner, in, out)
testutil.AssertErrorContains(t, err, testcase.wantError)
testutil.AssertRemediationErrorContains(t, err, testcase.wantRemediationError)
if testcase.wantOutputContains != "" {
testutil.AssertStringContains(t, buf.String(), testcase.wantOutputContains)
}
Expand Down Expand Up @@ -965,6 +1003,44 @@ func TestGetIdealPackage(t *testing.T) {
}
}

func TestGetLatestCrateVersion(t *testing.T) {
for _, testcase := range []struct {
name string
inputClient api.HTTPClient
wantVersion string
wantError string
}{
{
name: "http error",
inputClient: &errorClient{errTest},
wantError: "fixture error",
},
{
name: "no valid versions",
inputClient: &versionClient{[]string{}},
wantError: "no valid crate versions found",
},
{
name: "unsorted",
inputClient: &versionClient{[]string{"0.5.23", "0.1.0", "1.2.3", "0.7.3"}},
wantVersion: "1.2.3",
},
{
name: "reverse chronological",
inputClient: &versionClient{[]string{"1.2.3", "0.8.3", "0.3.2"}},
wantVersion: "1.2.3",
},
} {
t.Run(testcase.name, func(t *testing.T) {
v, err := compute.GetLatestCrateVersion(testcase.inputClient, "fastly")
testutil.AssertErrorContains(t, err, testcase.wantError)
if v != testcase.wantVersion {
t.Errorf("wanted version %s, got %s", testcase.wantVersion, v)
}
})
}
}

func makeInitEnvironment(t *testing.T) (rootdir string) {
t.Helper()

Expand All @@ -986,7 +1062,7 @@ func makeInitEnvironment(t *testing.T) (rootdir string) {
return rootdir
}

func makeBuildEnvironment(t *testing.T, manifestContent string) (rootdir string) {
func makeBuildEnvironment(t *testing.T, fastlyManifestContent, cargoManifestContent, cargoLockContent string) (rootdir string) {
t.Helper()

p := make([]byte, 8)
Expand Down Expand Up @@ -1014,9 +1090,23 @@ func makeBuildEnvironment(t *testing.T, manifestContent string) (rootdir string)
copyFile(t, fromFilename, toFilename)
}

if manifestContent != "" {
if fastlyManifestContent != "" {
filename := filepath.Join(rootdir, compute.ManifestFilename)
if err := ioutil.WriteFile(filename, []byte(manifestContent), 0777); err != nil {
if err := ioutil.WriteFile(filename, []byte(fastlyManifestContent), 0777); err != nil {
t.Fatal(err)
}
}

if cargoManifestContent != "" {
filename := filepath.Join(rootdir, "Cargo.toml")
if err := ioutil.WriteFile(filename, []byte(cargoManifestContent), 0777); err != nil {
t.Fatal(err)
}
}

if cargoLockContent != "" {
filename := filepath.Join(rootdir, "Cargo.lock")
if err := ioutil.WriteFile(filename, []byte(cargoLockContent), 0777); err != nil {
t.Fatal(err)
}
}
Expand Down Expand Up @@ -1230,3 +1320,22 @@ func (c codeClient) Do(*http.Request) (*http.Response, error) {
rec.WriteHeader(c.code)
return rec.Result(), nil
}

type versionClient struct {
versions []string
}

func (v versionClient) Do(*http.Request) (*http.Response, error) {
rec := httptest.NewRecorder()

var versions []string
for _, vv := range v.versions {
versions = append(versions, fmt.Sprintf(`{"num":"%s"}`, vv))
}

_, err := rec.Write([]byte(fmt.Sprintf(`{"versions":[%s]}`, strings.Join(versions, ","))))
if err != nil {
return nil, err
}
return rec.Result(), nil
}
Loading

0 comments on commit d5de911

Please sign in to comment.