Skip to content

Commit

Permalink
Verify fastly crate version is up-to-date during compute build.
Browse files Browse the repository at this point in the history
  • Loading branch information
phamann committed May 11, 2020
1 parent 817b5bd commit 4f54b8f
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 22 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) 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
28 changes: 27 additions & 1 deletion pkg/compute/compute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,36 +277,49 @@ func TestBuild(t *testing.T) {
name string
args []string
manifest string
client api.HTTPClient
wantError string
wantOutputContains string
}{
{
name: "no fastly.toml manifest",
args: []string{"compute", "build"},
client: versionClient{"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",
client: versionClient{"0.0.0"},
wantError: "language cannot be empty, please provide a language",
},
{
name: "empty name",
args: []string{"compute", "build"},
manifest: "language = \"rust\"\n",
client: versionClient{"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",
client: versionClient{"0.0.0"},
wantError: "unsupported language javascript",
},
{
name: "fastly crate out-of-date",
args: []string{"compute", "build"},
manifest: "name = \"test\"\nlanguage = \"rust\"\n",
client: versionClient{"0.4.0"},
wantError: "fastly crate not up-to-date",
},
{
name: "success",
args: []string{"compute", "build"},
manifest: "name = \"test\"\nlanguage = \"rust\"\n",
client: versionClient{"0.0.0"},
wantOutputContains: "Built rust package test",
},
} {
Expand Down Expand Up @@ -337,7 +350,7 @@ 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
Expand Down Expand Up @@ -1230,3 +1243,16 @@ func (c codeClient) Do(*http.Request) (*http.Response, error) {
rec.WriteHeader(c.code)
return rec.Result(), nil
}

type versionClient struct {
version string
}

func (v versionClient) Do(*http.Request) (*http.Response, error) {
rec := httptest.NewRecorder()
_, err := rec.Write([]byte(fmt.Sprintf(`{"versions":[{"num":"%s"}]}`, v.version)))
if err != nil {
return nil, err
}
return rec.Result(), nil
}
132 changes: 129 additions & 3 deletions pkg/compute/rust.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@ package compute
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"

"github.com/BurntSushi/toml"
"github.com/blang/semver"
"github.com/fastly/cli/pkg/api"
"github.com/fastly/cli/pkg/common"
"github.com/fastly/cli/pkg/errors"
"github.com/fastly/cli/pkg/text"
Expand All @@ -26,9 +31,11 @@ const (
)

// CargoPackage models the package confuiguration properties of a Rust Cargo
// package which we are interested in and is embedded within CargoManifest.
// package which we are interested in and is embedded within CargoManifest and
// CargoLock.
type CargoPackage struct {
Name string `toml:"name"`
Name string `toml:"name"`
Version string `toml:"version"`
}

// CargoManifest models the package confuiguration properties of a Rust Cargo
Expand All @@ -44,8 +51,23 @@ func (m *CargoManifest) Read(filename string) error {
return err
}

// CargoLock models the package confuiguration properties of a Rust Cargo
// lock file which we are interested in and are read from the Cargo.lock file
// within the $PWD of the package.
type CargoLock struct {
Package []CargoPackage
}

// Read the contents of the Cargo.lock file from filename.
func (m *CargoLock) Read(filename string) error {
_, err := toml.DecodeFile(filename, m)
return err
}

// Rust is an implments Toolchain for the Rust lanaguage.
type Rust struct{}
type Rust struct {
client api.HTTPClient
}

// Verify implments the Toolchain interface and verifies whether the Rust
// language toolchain is correctly configured on the host.
Expand Down Expand Up @@ -148,6 +170,51 @@ func (r Rust) Verify(out io.Writer) error {

fmt.Fprintf(out, "Found Cargo.toml at %s\n", fpath)

// 5) Verify `fastly` crate version
//
// A valid and up-to-date version of the fastly crate is required.
fpath, err = filepath.Abs("Cargo.lock")
if err != nil {
return fmt.Errorf("error getting Cargo.lock path: %w", err)
}

if !common.FileExists(fpath) {
return fmt.Errorf("%s not found", fpath)
}

var lock CargoLock
if err := lock.Read("Cargo.lock"); err != nil {
return fmt.Errorf("error reading Cargo.lock file: %w", err)
}

var crate CargoPackage
for _, p := range lock.Package {
if p.Name == "fastly" {
crate = p
}
}

version, err := semver.Parse(crate.Version)
if err != nil {
return fmt.Errorf("error parsing Cargo.lock file: %w", err)
}

l, err := GetLatestCrateVersion(r.client, "fastly")
if err != nil {
return err
}
latest, err := semver.Parse(l)
if err != nil {
return err
}

if version.LT(latest) {
return errors.RemediationError{
Inner: fmt.Errorf("fastly crate not up-to-date"),
Remediation: fmt.Sprintf("To fix this error, run the following command:\n\n\t$ %s\n", text.Bold("cargo update -p fastly")),
}
}

return nil
}

Expand Down Expand Up @@ -243,3 +310,62 @@ func (r Rust) Build(out io.Writer) error {

return nil
}

// CargoCrateVersion models a Cargo crate version returned by the crates.io API.
type CargoCrateVersion struct {
Version string `json:"num"`
}

// CargoCrateVersions models a Cargo crate version returned by the crates.io API.
type CargoCrateVersions struct {
Versions []CargoCrateVersion `json:"versions"`
}

// GetLatestCrateVersion fetches all versions of a given Rust crate from the
// crates.io HTTP API and returns the latest valid semver version.
func GetLatestCrateVersion(client api.HTTPClient, name string) (string, error) {
url := fmt.Sprintf("https://crates.io/api/v1/crates/%s/versions", name)

req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", err
}
resp, err := client.Do(req)
if err != nil {
return "", err
}

if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("error fetching latest crate version %s", resp.Status)
}

defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}

crate := CargoCrateVersions{}
err = json.Unmarshal(body, &crate)
if err != nil {
return "", err
}

var versions []semver.Version
for _, v := range crate.Versions {
if version, err := semver.Parse(v.Version); err == nil {
versions = append(versions, version)
}
}

if len(versions) < 1 {
return "", fmt.Errorf("no valid crate versions found")
}

semver.Sort(versions)

latest := versions[len(versions)-1]

return latest.String(), nil
}
5 changes: 4 additions & 1 deletion pkg/compute/testdata/build/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pkg/compute/testdata/build/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ edition = "2018"
debug = true

[dependencies]
fastly = "^0.3.2"

0 comments on commit 4f54b8f

Please sign in to comment.