Skip to content

feat(product): Add 'product' commands for each Fastly product. #1362

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

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
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
17 changes: 9 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ ifeq ($(OS), Windows_NT)
.SHELLFLAGS = /c
GO_FILES = $(shell where /r pkg *.go)
GO_FILES += $(shell where /r cmd *.go)
GO_FILES += $(shell where /r internal *.go)
CONFIG_SCRIPT = scripts\config.sh
CONFIG_FILE = pkg\config\config.toml
else
GO_FILES = $(shell find cmd pkg -type f -name '*.go')
GO_FILES = $(shell find cmd pkg internal -type f -name '*.go')
CONFIG_SCRIPT = ./scripts/config.sh
CONFIG_FILE = pkg/config/config.toml
endif
Expand Down Expand Up @@ -72,13 +73,13 @@ tidy:
# Run formatter.
.PHONY: fmt
fmt:
@echo gofmt -l ./{cmd,pkg}
@eval "bash -c 'F=\$$(gofmt -l ./{cmd,pkg}) ; if [[ \$$F ]] ; then echo \$$F ; exit 1 ; fi'"
@echo gofmt -l ./{cmd,pkg,internal}
@eval "bash -c 'F=\$$(gofmt -l ./{cmd,pkg,internal}) ; if [[ \$$F ]] ; then echo \$$F ; exit 1 ; fi'"

# Run static analysis.
.PHONY: vet
vet: config ## Run vet static analysis
$(GO_BIN) vet ./{cmd,pkg}/...
$(GO_BIN) vet ./{cmd,pkg,internal}/...

# Run linter.
.PHONY: revive
Expand All @@ -88,7 +89,7 @@ revive: ## Run linter (using revive)
# Run security vulnerability checker.
.PHONY: gosec
gosec: ## Run security vulnerability checker
gosec -quiet -exclude=G104 ./{cmd,pkg}/...
gosec -quiet -exclude=G104 ./{cmd,pkg,internal}/...

nilaway: ## Run nilaway
@nilaway ./...
Expand All @@ -105,13 +106,13 @@ semgrep: ## Run semgrep
# To ignore lines use: //lint:ignore <CODE> <REASON>
.PHONY: staticcheck
staticcheck: ## Run static analysis
staticcheck ./{cmd,pkg}/...
staticcheck ./{cmd,pkg,internal}/...

# Run imports formatter.
.PHONY: imports
imports:
@echo goimports ./{cmd,pkg}
@eval "bash -c 'F=\$$(goimports -l ./{cmd,pkg}) ; if [[ \$$F ]] ; then echo \$$F ; exit 1 ; fi'"
@echo goimports ./{cmd,pkg,internal}
@eval "bash -c 'F=\$$(goimports -l ./{cmd,pkg,internal}) ; if [[ \$$F ]] ; then echo \$$F ; exit 1 ; fi'"

.PHONY: golangci
golangci: ## Run golangci-lint
Expand Down
106 changes: 106 additions & 0 deletions internal/productcore/base.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package productcore

import (
"fmt"

"github.com/fastly/cli/pkg/api"
"github.com/fastly/cli/pkg/argparser"
"github.com/fastly/cli/pkg/global"
"github.com/fastly/cli/pkg/manifest"
"github.com/fastly/go-fastly/v9/fastly/products"
)

// Base is a base type for all product commands.
type Base struct {
argparser.Base
argparser.JSONOutput
Manifest manifest.Data

ServiceName argparser.OptionalServiceNameID
}

// Init prepares the structure for use by the CLI core.
func (cmd *Base) Init(parent argparser.Registerer, g *global.Data) {
cmd.Globals = g

// Optional flags.
cmd.RegisterFlag(argparser.StringFlagOpts{
Name: argparser.FlagServiceIDName,
Description: argparser.FlagServiceIDDesc,
Dst: &g.Manifest.Flag.ServiceID,
Short: 's',
})
cmd.RegisterFlag(argparser.StringFlagOpts{
Action: cmd.ServiceName.Set,
Name: argparser.FlagServiceName,
Description: argparser.FlagServiceNameDesc,
Dst: &cmd.ServiceName.Value,
})
cmd.RegisterFlagBool(cmd.JSONFlag()) // --json
}

// EnablementStatus is a structure used to generate output from
// the enablement-related commands
type EnablementStatus[_ products.ProductOutput] struct {
ProductName string `json:"-"`
ProductID string `json:"product_id"`
ServiceID string `json:"service_id"`
Enabled bool `json:"enabled"`
}

type StatusManager[O products.ProductOutput] interface {
SetEnabled(bool)
GetEnabled() string
GetProductName() string
SetProductID(string)
SetServiceID(string)
TransformOutput(O)
GetTextResult() string
}

func (s *EnablementStatus[_]) SetEnabled(e bool) {
s.Enabled = e
}

func (s *EnablementStatus[_]) GetEnabled() string {
if s.Enabled {
return "enabled"
}
return "disabled"
}

func (s *EnablementStatus[_]) GetProductName() string {
return s.ProductName
}

func (s *EnablementStatus[_]) SetProductID(id string) {
s.ProductID = id
}

func (s *EnablementStatus[_]) SetServiceID(id string) {
s.ServiceID = id
}

func (s *EnablementStatus[O]) TransformOutput(o O) {
s.ProductID = o.ProductID()
s.ServiceID = o.ServiceID()
}

func (s *EnablementStatus[O]) GetTextResult() string {
return fmt.Sprintf("%s is %s on service %s", s.ProductName, s.GetEnabled(), s.ServiceID)
}

// EnablementHookFuncs is a structure of dependency-injection points
// used by unit tests to provide mock behaviors
type EnablementHookFuncs[O products.ProductOutput] struct {
DisableFunc func(api.Interface, string) error
EnableFunc func(api.Interface, string) (O, error)
GetFunc func(api.Interface, string) (O, error)
}

// ConfigurationHookFuncs is a structure of dependency-injection
// points used by unit tests to provide mock behaviors
type ConfigurationHookFuncs[O, I any] struct {
GetConfigurationFunc func(api.Interface, string) (O, error)
UpdateConfigurationFunc func(api.Interface, string, I) (O, error)
}
69 changes: 69 additions & 0 deletions internal/productcore/disable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package productcore

import (
"io"

fsterr "github.com/fastly/cli/pkg/errors"

"github.com/fastly/cli/pkg/argparser"
"github.com/fastly/cli/pkg/global"
"github.com/fastly/cli/pkg/text"
"github.com/fastly/go-fastly/v9/fastly/products"
)

// Disable is a base type for all 'disable' commands.
type Disable[O products.ProductOutput, _ StatusManager[O]] struct {
Base
ProductID string
// hooks is a pointer to an EnablementHookFuncs structure so
// that tests can modify the contents of the structure after
// this structure has been initialized
hooks *EnablementHookFuncs[O]
}

// Init prepares the structure for use by the CLI core.
func (cmd *Disable[O, _]) Init(parent argparser.Registerer, g *global.Data, productID, productName string, hooks *EnablementHookFuncs[O]) {
cmd.CmdClause = parent.Command("disable", "Disable the "+productName+" product")
cmd.hooks = hooks

cmd.Base.Init(parent, g)
cmd.ProductID = productID
}

// Exec executes the disablement operation.
func (cmd *Disable[O, S]) Exec(out io.Writer, status S) error {
if cmd.Globals.Verbose() && cmd.JSONOutput.Enabled {
return fsterr.ErrInvalidVerboseJSONCombo
}

serviceID, source, flag, err := argparser.ServiceID(cmd.ServiceName, *cmd.Globals.Manifest, cmd.Globals.APIClient, cmd.Globals.ErrLog)
if err != nil {
cmd.Globals.ErrLog.Add(err)
return err
}

if cmd.Globals.Verbose() {
argparser.DisplayServiceID(serviceID, flag, source, out)
}

err = cmd.hooks.DisableFunc(cmd.Globals.APIClient, serviceID)
if err != nil {
cmd.Globals.ErrLog.Add(err)
return err
}

status.SetEnabled(false)
// The API does not return details of the service and product
// which were disabled, so they have to be inserted into
// 'status' directly
status.SetProductID(cmd.ProductID)
status.SetServiceID(serviceID)

if ok, err := cmd.WriteJSON(out, status); ok {
return err
}

text.Success(out, status.GetTextResult())

return nil
}
4 changes: 4 additions & 0 deletions internal/productcore/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Package productcore contains a group of generic structures and
// functions which are used to implement common behaviors in product
// enablement/configuration commands
package productcore
62 changes: 62 additions & 0 deletions internal/productcore/enable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package productcore

import (
"io"

"github.com/fastly/cli/pkg/argparser"
fsterr "github.com/fastly/cli/pkg/errors"
"github.com/fastly/cli/pkg/global"
"github.com/fastly/cli/pkg/text"
"github.com/fastly/go-fastly/v9/fastly/products"
)

// Enable is a base type for all 'enable' commands.
type Enable[O products.ProductOutput, _ StatusManager[O]] struct {
Base
// hooks is a pointer to an EnablementHookFuncs structure so
// that tests can modify the contents of the structure after
// this structure has been initialized
hooks *EnablementHookFuncs[O]
}

// Init prepares the structure for use by the CLI core.
func (cmd *Enable[O, _]) Init(parent argparser.Registerer, g *global.Data, productName string, hooks *EnablementHookFuncs[O]) {
cmd.CmdClause = parent.Command("enable", "Enable the "+productName+" product")
cmd.hooks = hooks

cmd.Base.Init(parent, g)
}

// Exec executes the enablement operation.
func (cmd *Enable[O, S]) Exec(out io.Writer, status S) error {
if cmd.Globals.Verbose() && cmd.JSONOutput.Enabled {
return fsterr.ErrInvalidVerboseJSONCombo
}

serviceID, source, flag, err := argparser.ServiceID(cmd.ServiceName, *cmd.Globals.Manifest, cmd.Globals.APIClient, cmd.Globals.ErrLog)
if err != nil {
cmd.Globals.ErrLog.Add(err)
return err
}

if cmd.Globals.Verbose() {
argparser.DisplayServiceID(serviceID, flag, source, out)
}

o, err := cmd.hooks.EnableFunc(cmd.Globals.APIClient, serviceID)
if err != nil {
cmd.Globals.ErrLog.Add(err)
return err
}

status.SetEnabled(true)
status.TransformOutput(o)

if ok, err := cmd.WriteJSON(out, status); ok {
return err
}

text.Success(out, status.GetTextResult())

return nil
}
71 changes: 71 additions & 0 deletions internal/productcore/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package productcore

import (
"errors"
"io"

"github.com/fastly/cli/pkg/argparser"
fsterr "github.com/fastly/cli/pkg/errors"
"github.com/fastly/cli/pkg/global"
"github.com/fastly/cli/pkg/text"
"github.com/fastly/go-fastly/v9/fastly"
"github.com/fastly/go-fastly/v9/fastly/products"
)

// Status is a base type for all 'status' commands.
type Status[O products.ProductOutput, _ StatusManager[O]] struct {
Base
// hooks is a pointer to an EnablementHookFuncs structure so
// that tests can modify the contents of the structure after
// this structure has been initialized
hooks *EnablementHookFuncs[O]
}

// Init prepares the structure for use by the CLI core.
func (cmd *Status[O, _]) Init(parent argparser.Registerer, g *global.Data, productName string, hooks *EnablementHookFuncs[O]) {
cmd.CmdClause = parent.Command("status", "Get the enablement status of the "+productName+" product")
cmd.hooks = hooks

cmd.Base.Init(parent, g)
}

// Exec executes the status operation.
func (cmd *Status[O, S]) Exec(out io.Writer, status S) error {
if cmd.Globals.Verbose() && cmd.JSONOutput.Enabled {
return fsterr.ErrInvalidVerboseJSONCombo
}

serviceID, source, flag, err := argparser.ServiceID(cmd.ServiceName, *cmd.Globals.Manifest, cmd.Globals.APIClient, cmd.Globals.ErrLog)
if err != nil {
cmd.Globals.ErrLog.Add(err)
return err
}

if cmd.Globals.Verbose() {
argparser.DisplayServiceID(serviceID, flag, source, out)
}

o, err := cmd.hooks.GetFunc(cmd.Globals.APIClient, serviceID)
if err != nil {
var herr *fastly.HTTPError

// The API returns a 'Bad Request' error when the
// product has not been enabled on the service; any
// other error should be reported
if !errors.As(err, &herr) || !herr.IsBadRequest() {
cmd.Globals.ErrLog.Add(err)
return err
}
} else {
status.SetEnabled(true)
status.TransformOutput(o)
}

if ok, err := cmd.WriteJSON(out, status); ok {
return err
}

text.Info(out, status.GetTextResult())

return nil
}
4 changes: 4 additions & 0 deletions internal/productcore_test/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Package productcore_test contains a group of generic structures and
// functions which are used to implement common behaviors in product
// enablement/configuration tests
package productcore_test
Loading
Loading