Skip to content

Commit

Permalink
Define new --version and --autoclone flags (#300)
Browse files Browse the repository at this point in the history
* add missing function to api.Interface

* service doc string typo

* add missing function to mock.API

* implement new --version and --autoclone flags

* address feedback

* update messages in the tests

* add tests for parsing logic

* remove editable flag value and default to latest

* update tests to validate new flag behaviours

* update flag descriptions

* rename functions to set rather than new to avoid confusion as a constructor

* correct description of getLatestActiveVersion function

* refactor errMatches function

* display message to user when cloning

* typo in doc string

* rename getLatestActiveVersion

* fix test panic caused by not setting required Client field

* only display clone message when in verbose mode
  • Loading branch information
Integralist committed Jun 7, 2021
1 parent 811c515 commit 24734d4
Show file tree
Hide file tree
Showing 6 changed files with 492 additions and 9 deletions.
5 changes: 1 addition & 4 deletions pkg/api/interface.go
Expand Up @@ -16,10 +16,6 @@ type HTTPClient interface {

// Interface models the methods of the Fastly API client that we use.
// It exists to allow for easier testing, in combination with Mock.
//
// TODO(integralist):
// There are missing methods such as GetVersion from this list so review in
// future the missing features in CLI and implement here.
type Interface interface {
GetTokenSelf() (*fastly.Token, error)

Expand All @@ -33,6 +29,7 @@ type Interface interface {

CloneVersion(*fastly.CloneVersionInput) (*fastly.Version, error)
ListVersions(*fastly.ListVersionsInput) ([]*fastly.Version, error)
GetVersion(*fastly.GetVersionInput) (*fastly.Version, error)
UpdateVersion(*fastly.UpdateVersionInput) (*fastly.Version, error)
ActivateVersion(*fastly.ActivateVersionInput) (*fastly.Version, error)
DeactivateVersion(*fastly.DeactivateVersionInput) (*fastly.Version, error)
Expand Down
148 changes: 148 additions & 0 deletions pkg/cmd/flags.go
@@ -0,0 +1,148 @@
package cmd

import (
"fmt"
"io"
"sort"
"strconv"
"strings"

"github.com/fastly/cli/pkg/api"
"github.com/fastly/cli/pkg/text"
"github.com/fastly/go-fastly/v3/fastly"
"github.com/fastly/kingpin"
)

// ServiceVersionFlagOpts enables easy configuration of the --version flag
// defined via the NewServiceVersionFlag constructor.
//
// NOTE: The reason we define an 'optional' field rather than a 'required'
// field is because 99% of the use cases where --version is defined the flag
// will be required, and so we cater for the common case. Meaning only those
// subcommands that have --version as optional will need to set that field.
type ServiceVersionFlagOpts struct {
Dst *string
Optional bool
Action kingpin.Action
}

// SetServiceVersionFlag defines a --version flag that accepts multiple values
// such as 'latest', 'active' and numerical values which are then converted
// into the appropriate service version.
func (b Base) SetServiceVersionFlag(opts ServiceVersionFlagOpts, args ...string) {
clause := b.CmdClause.Flag("version", "'latest', 'active', or the number of a specific version")
if !opts.Optional {
clause = clause.Required()
} else {
clause = clause.Action(opts.Action)
}
clause.StringVar(opts.Dst)
}

// SetAutoCloneFlag defines a --autoclone flag that will cause a clone of the
// identified service version if it's found to be active or locked.
func (b Base) SetAutoCloneFlag(action kingpin.Action, dst *bool) {
b.CmdClause.Flag("autoclone", "If the selected service version is not editable, clone it and use the clone.").Action(action).BoolVar(dst)
}

// OptionalServiceVersion represents a Fastly service version.
type OptionalServiceVersion struct {
OptionalString
Client api.Interface
}

// Parse returns a service version based on the given user input.
func (sv *OptionalServiceVersion) Parse(sid string) (*fastly.Version, error) {
vs, err := sv.Client.ListVersions(&fastly.ListVersionsInput{
ServiceID: sid,
})
if err != nil || len(vs) == 0 {
return nil, fmt.Errorf("error listing service versions: %w", err)
}

// Sort versions into descending order.
sort.Slice(vs, func(i, j int) bool {
return vs[i].Number > vs[j].Number
})

var v *fastly.Version

switch strings.ToLower(sv.Value) {
case "latest":
return vs[0], nil
case "active":
v, err = getActiveVersion(vs)
case "":
return vs[0], nil
default:
v, err = getSpecifiedVersion(vs, sv.Value)
}
if err != nil {
return nil, err
}

return v, nil
}

// OptionalAutoClone defines a method set for abstracting the logic required to
// identify if a given service version needs to be cloned.
type OptionalAutoClone struct {
OptionalBool
Out io.Writer
Client api.Interface
}

// Parse returns a service version.
//
// The returned version is either the same as the input argument `v` or it's a
// cloned version if the input argument was either active or locked.
func (ac *OptionalAutoClone) Parse(v *fastly.Version, sid string, verbose bool) (*fastly.Version, error) {
// if user didn't provide --autoclone flag
if !ac.Value && (v.Active || v.Locked) {
return nil, fmt.Errorf("service version %d is not editable", v.Number)
}
if ac.Value && (v.Active || v.Locked) {
version, err := ac.Client.CloneVersion(&fastly.CloneVersionInput{
ServiceID: sid,
ServiceVersion: v.Number,
})
if err != nil {
return nil, fmt.Errorf("error cloning service version: %w", err)
}
if verbose {
msg := fmt.Sprintf("Service version %d is not editable, so it was automatically cloned because --autoclone is enabled. Now operating on version %d.", v.Number, version.Number)
text.Output(ac.Out, msg)
text.Break(ac.Out)
}
return version, nil
}

// Treat the function as a no-op if the version is editable.
return v, nil
}

// getActiveVersion returns the active service version.
func getActiveVersion(vs []*fastly.Version) (*fastly.Version, error) {
for _, v := range vs {
if v.Active {
return v, nil
}
}
return nil, fmt.Errorf("no active service version found")
}

// getSpecifiedVersion returns the specified service version.
func getSpecifiedVersion(vs []*fastly.Version, version string) (*fastly.Version, error) {
i, err := strconv.Atoi(version)
if err != nil {
return nil, err
}

for _, v := range vs {
if v.Number == i {
return v, nil
}
}

return nil, fmt.Errorf("specified service version not found: %s", version)
}

0 comments on commit 24734d4

Please sign in to comment.