Skip to content

Commit

Permalink
Interactive init (#5)
Browse files Browse the repository at this point in the history
* Bump go-fastly to v1.7.0

* Add golang-petname dep

* Add NullProgress writer

* Add UndoStack to pkg/common

* Persist user email to global config file

* Make `compute init` accept input interactively and add logic to provision service, domain and backend during initialization.

* Surpress gosec G307 on file.Close() defers

* Add RunIfError method to UndoStack which unwinds the stack if passed an error

* Add text.Description() output helper to print term:description groupings.

* Refactor compute init to use new helpers
  • Loading branch information
phamann committed Mar 11, 2020
1 parent 2eee737 commit 2cc45a5
Show file tree
Hide file tree
Showing 18 changed files with 656 additions and 83 deletions.
1 change: 1 addition & 0 deletions .github/workflows/pr_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ jobs:
run: make test
shell: bash
env:
TEST_COMPUTE_INIT: true
TEST_COMPUTE_BUILD: true
scan:
runs-on: ubuntu-latest
Expand Down
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ require (
github.com/ajg/form v1.5.1 // indirect
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
github.com/blang/semver v3.5.1+incompatible
github.com/fastly/go-fastly v1.3.0
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0
github.com/fastly/go-fastly v1.7.0
github.com/fatih/color v1.7.0
github.com/frankban/quicktest v1.5.0 // indirect
github.com/google/go-cmp v0.3.1
github.com/google/go-github/v28 v28.1.1
github.com/google/jsonapi v0.0.0-20181016150055-d0428f63eb51 // indirect
github.com/google/jsonapi v0.0.0-20200226002910-c8283f632fb7 // indirect
github.com/hashicorp/go-cleanhttp v0.5.1 // indirect
github.com/kennygrant/sanitize v1.2.4
github.com/mattn/go-colorable v0.1.4 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyG
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 h1:90Ly+6UfUypEF6vvvW5rQIv9opIL8CbmW9FT20LDQoY=
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0/go.mod h1:V+Qd57rJe8gd4eiGzZyg4h54VLHmYVVw54iMnlAMrF8=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/fastly/go-fastly v1.3.0 h1:halI6tkEmn5JIeW888P2bNGprGP7OYot+o1akPXvPyY=
github.com/fastly/go-fastly v1.3.0/go.mod h1:cBtWXhszIFx9xpzgm9L/3PW3Ixszo153xJBEHJghGUk=
github.com/fastly/go-fastly v1.7.0 h1:IiDMXzoSSJZjQP5VIjv+BU4WMoo3DeMGIy2xm9BDuL8=
github.com/fastly/go-fastly v1.7.0/go.mod h1:cBtWXhszIFx9xpzgm9L/3PW3Ixszo153xJBEHJghGUk=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
Expand All @@ -52,6 +56,8 @@ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO
github.com/google/jsonapi v0.0.0-20170708005851-46d3ced04344/go.mod h1:XSx4m2SziAqk9DXY9nz659easTq4q6TyrpYd9tHSm0g=
github.com/google/jsonapi v0.0.0-20181016150055-d0428f63eb51 h1:k+U8IQj6kj659R+Ahq6YsK03GdUo8qQdTsq5HBzfQwM=
github.com/google/jsonapi v0.0.0-20181016150055-d0428f63eb51/go.mod h1:XSx4m2SziAqk9DXY9nz659easTq4q6TyrpYd9tHSm0g=
github.com/google/jsonapi v0.0.0-20200226002910-c8283f632fb7 h1:aQ4kMXDAmP9IRIZHcSKB2orXHGwGiSxH4PX1BzKHR50=
github.com/google/jsonapi v0.0.0-20200226002910-c8283f632fb7/go.mod h1:XSx4m2SziAqk9DXY9nz659easTq4q6TyrpYd9tHSm0g=
github.com/hashicorp/go-cleanhttp v0.0.0-20170211013415-3573b8b52aa7/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
Expand Down
2 changes: 2 additions & 0 deletions pkg/api/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ type Interface interface {
GetBigQuery(*fastly.GetBigQueryInput) (*fastly.BigQuery, error)
UpdateBigQuery(*fastly.UpdateBigQueryInput) (*fastly.BigQuery, error)
DeleteBigQuery(*fastly.DeleteBigQueryInput) error

GetUser(*fastly.GetUserInput) (*fastly.User, error)
}

// Interface assertion, to catch mismatches early.
Expand Down
20 changes: 11 additions & 9 deletions pkg/app/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,15 +245,17 @@ COMMANDS
compute init [<flags>]
Initialize a new Compute@Edge package locally
-n, --name=NAME Name of package, defaulting to directory name
of the --path destination
-f, --from="https://github.com/fastly/fastly-template-rust-default"
Git repository containing package template
-p, --path=PATH Destination to write the new package,
defaulting to the current directory
-s, --service-id=SERVICE-ID Optional Fastly service ID written to the
package manifest, where this package will be
deployed
-n, --name=NAME Name of package, defaulting to directory name
of the --path destination
-d, --description=DESCRIPTION Description of the package
-a, --author=AUTHOR Author of the package
-f, --from=FROM Git repository containing package template
-p, --path=PATH Destination to write the new package,
defaulting to the current directory
--domain=DOMAIN The name of the domain associated to the
package
--backend=BACKEND A hostname, IPv4, or IPv6 address for the
package backend
compute build [<flags>]
Build a Compute@Edge package locally
Expand Down
2 changes: 1 addition & 1 deletion pkg/common/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func CopyFile(src, dst string) (err error) {
if err != nil {
return fmt.Errorf("error reading source file: %w", err)
}
defer in.Close()
defer in.Close() // #nosec G307

// Create destination file for writing.
out, err := os.Create(dst)
Expand Down
66 changes: 66 additions & 0 deletions pkg/common/undo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package common

import (
"fmt"
"io"
)

// UndoFn is a function with no arguments which returns an error or nil.
type UndoFn func() error

// UndoStack models a simple undo stack which consumers can use to store undo
// stateful functions, such as a function to teardown API state if something
// goes wrong during procedural commands, for example deleting a Fastly service
// after it's been created.
type UndoStack struct {
states []UndoFn
}

// NewUndoStack constructs a new UndoStack.
func NewUndoStack() *UndoStack {
s := make([]UndoFn, 0, 1)
stack := &UndoStack{
states: s,
}
return stack
}

// Pop method pops last added UndoFn element oof the stack and returns it.
// If stack is empty Pop() returns nil.
func (s *UndoStack) Pop() UndoFn {
n := len(s.states)
if n == 0 {
return nil
}
v := s.states[n-1]
s.states = s.states[:n-1]
return v
}

// Push method pushes an Undoer element onto the UndoStack.
func (s *UndoStack) Push(elem UndoFn) {
s.states = append(s.states, elem)
}

// Len method returns the number of elements in the UndoStack.
func (s *UndoStack) Len() int {
return len(s.states)
}

// RunIfError unwinds the stack if a non-nil error is passed, by serially
// calling each UndoFn function state in FIFO order. If any UndoFn returns an
// error, it gets logged to the provided writer. Should be deferrerd, such as:
//
// undoStack := common.NewUndoStack()
// defer undoStack.RunIfError(w, err)
//
func (s *UndoStack) RunIfError(w io.Writer, err error) {
if err == nil {
return
}
for i := len(s.states) - 1; i >= 0; i-- {
if err := s.states[i](); err != nil {
fmt.Fprintf(w, "%w", err)
}
}
}
180 changes: 167 additions & 13 deletions pkg/compute/compute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,86 @@ func TestInit(t *testing.T) {
for _, testcase := range []struct {
name string
args []string
configFile config.File
api mock.API
wantFiles []string
unwantedFiles []string
stdin string
wantError string
wantOutput []string
manifestIncludes string
}{
{
name: "unkown repository",
args: []string{"compute", "init", "--from", "https://example.com/template"},
name: "no token",
args: []string{"compute", "init"},
wantError: "no token provided",
},
{
name: "unkown repository",
args: []string{"compute", "init", "--from", "https://example.com/template"},
configFile: config.File{Token: "123"},
api: mock.API{
GetTokenSelfFn: tokenOK,
GetUserFn: getUserOk,
CreateServiceFn: createServiceOK,
CreateDomainFn: createDomainOK,
CreateBackendFn: createBackendOK,
DeleteServiceFn: deleteServiceOK,
DeleteBackendFn: deleteBackendOK,
DeleteDomainFn: deleteDomainOK,
},
wantError: "error fetching package template: repository not found",
},
{
name: "with name",
args: []string{"compute", "init", "--name", "test"},
name: "create service error",
args: []string{"compute", "init"},
configFile: config.File{Token: "123"},
api: mock.API{
GetTokenSelfFn: tokenOK,
GetUserFn: getUserOk,
CreateServiceFn: createServiceError,
},
wantError: "error creating service: fixture error",
},
{
name: "create domain error",
args: []string{"compute", "init"},
configFile: config.File{Token: "123"},
api: mock.API{
GetTokenSelfFn: tokenOK,
GetUserFn: getUserOk,
CreateServiceFn: createServiceOK,
CreateDomainFn: createDomainError,
DeleteServiceFn: deleteServiceOK,
},
wantError: "error creating domain: fixture error",
},
{
name: "create backend error",
args: []string{"compute", "init"},
configFile: config.File{Token: "123"},
api: mock.API{
GetTokenSelfFn: tokenOK,
GetUserFn: getUserOk,
CreateServiceFn: createServiceOK,
CreateDomainFn: createDomainOK,
CreateBackendFn: createBackendError,
DeleteServiceFn: deleteServiceOK,
DeleteDomainFn: deleteDomainOK,
},
wantError: "error creating backend: fixture error",
},
{
name: "with name",
args: []string{"compute", "init", "--name", "test"},
configFile: config.File{Token: "123"},
api: mock.API{
GetTokenSelfFn: tokenOK,
GetUserFn: getUserOk,
CreateServiceFn: createServiceOK,
CreateDomainFn: createDomainOK,
CreateBackendFn: createBackendOK,
},
wantOutput: []string{
"Initializing...",
"Fetching package template...",
Expand All @@ -55,20 +121,54 @@ func TestInit(t *testing.T) {
manifestIncludes: `name = "test"`,
},
{
name: "with service",
args: []string{"compute", "init", "--service-id", "test"},
name: "with description",
args: []string{"compute", "init", "--description", "test"},
configFile: config.File{Token: "123"},
api: mock.API{
GetTokenSelfFn: tokenOK,
GetUserFn: getUserOk,
CreateServiceFn: createServiceOK,
CreateDomainFn: createDomainOK,
CreateBackendFn: createBackendOK,
},
wantOutput: []string{
"Initializing...",
"Fetching package template...",
"Updating package manifest..",
},
manifestIncludes: `description = "test"`,
},
{
name: "with author",
args: []string{"compute", "init", "--author", "test@example.com"},
configFile: config.File{Token: "123"},
api: mock.API{
GetTokenSelfFn: tokenOK,
GetUserFn: getUserOk,
CreateServiceFn: createServiceOK,
CreateDomainFn: createDomainOK,
CreateBackendFn: createBackendOK,
},
wantOutput: []string{
"Initializing...",
"Fetching package template...",
"Updating package manifest..",
},
manifestIncludes: `service_id = "test"`,
manifestIncludes: `authors = ["test@example.com"]`,
},
{
name: "default",
args: []string{"compute", "init"},
name: "default",
args: []string{"compute", "init"},
configFile: config.File{Token: "123"},
api: mock.API{
GetTokenSelfFn: tokenOK,
GetUserFn: getUserOk,
CreateServiceFn: createServiceOK,
CreateDomainFn: createDomainOK,
CreateBackendFn: createBackendOK,
},
wantFiles: []string{
"cargo.toml",
"Cargo.toml",
"fastly.toml",
"src/main.rs",
},
Expand Down Expand Up @@ -108,12 +208,12 @@ func TestInit(t *testing.T) {
var (
args = testcase.args
env = config.Environment{}
file = config.File{}
file = testcase.configFile
appConfigFile = "/dev/null"
clientFactory = mock.APIClient(mock.API{})
clientFactory = mock.APIClient(testcase.api)
httpClient = http.DefaultClient
versioner update.Versioner = nil
in io.Reader = nil
in io.Reader = bytes.NewBufferString(testcase.stdin)
buf bytes.Buffer
out io.Writer = common.NewSyncWriter(&buf)
)
Expand Down Expand Up @@ -836,6 +936,60 @@ func copyFile(t *testing.T, fromFilename, toFilename string) {

var errTest = errors.New("fixture error")

func tokenOK() (*fastly.Token, error) { return &fastly.Token{}, nil }

func getUserOk(i *fastly.GetUserInput) (*fastly.User, error) {
return &fastly.User{Login: "test@example.com"}, nil
}

func createServiceOK(i *fastly.CreateServiceInput) (*fastly.Service, error) {
return &fastly.Service{
ID: "12345",
Name: i.Name,
Type: i.Type,
}, nil
}

func createServiceError(*fastly.CreateServiceInput) (*fastly.Service, error) {
return nil, errTest
}

func deleteServiceOK(i *fastly.DeleteServiceInput) error {
return nil
}

func createDomainOK(i *fastly.CreateDomainInput) (*fastly.Domain, error) {
return &fastly.Domain{
ServiceID: i.Service,
Version: i.Version,
Name: i.Name,
}, nil
}

func createDomainError(i *fastly.CreateDomainInput) (*fastly.Domain, error) {
return nil, errTest
}

func deleteDomainOK(i *fastly.DeleteDomainInput) error {
return nil
}

func createBackendOK(i *fastly.CreateBackendInput) (*fastly.Backend, error) {
return &fastly.Backend{
ServiceID: i.Service,
Version: i.Version,
Name: i.Name,
}, nil
}

func createBackendError(i *fastly.CreateBackendInput) (*fastly.Backend, error) {
return nil, errTest
}

func deleteBackendOK(i *fastly.DeleteBackendInput) error {
return nil
}

func latestVersionInactiveOk(i *fastly.LatestVersionInput) (*fastly.Version, error) {
return &fastly.Version{ServiceID: i.Service, Number: 1, Active: false}, nil
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/compute/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func (c *Client) UpdatePackage(serviceID string, v int, path string) error {
if err != nil {
return fmt.Errorf("error reading package: %w", err)
}
defer file.Close()
defer file.Close() // #nosec G307

var body bytes.Buffer
w := multipart.NewWriter(&body)
Expand Down
Loading

0 comments on commit 2cc45a5

Please sign in to comment.