Skip to content

Commit

Permalink
feat(product_enablement): add products command
Browse files Browse the repository at this point in the history
  • Loading branch information
Integralist committed Oct 9, 2023
1 parent 2b85f0e commit 63e66a6
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 0 deletions.
4 changes: 4 additions & 0 deletions pkg/api/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,10 @@ type Interface interface {
GetCondition(i *fastly.GetConditionInput) (*fastly.Condition, error)
ListConditions(i *fastly.ListConditionsInput) ([]*fastly.Condition, error)
UpdateCondition(i *fastly.UpdateConditionInput) (*fastly.Condition, error)

GetProduct(i *fastly.ProductEnablementInput) (*fastly.ProductEnablement, error)
EnableProduct(i *fastly.ProductEnablementInput) (*fastly.ProductEnablement, error)
DisableProduct(i *fastly.ProductEnablementInput) error
}

// RealtimeStatsInterface is the subset of go-fastly's realtime stats API used here.
Expand Down
3 changes: 3 additions & 0 deletions pkg/app/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import (
"github.com/fastly/cli/pkg/commands/logging/syslog"
"github.com/fastly/cli/pkg/commands/logtail"
"github.com/fastly/cli/pkg/commands/pop"
"github.com/fastly/cli/pkg/commands/products"
"github.com/fastly/cli/pkg/commands/profile"
"github.com/fastly/cli/pkg/commands/purge"
"github.com/fastly/cli/pkg/commands/ratelimit"
Expand Down Expand Up @@ -329,6 +330,7 @@ func defineCommands(
loggingSyslogList := syslog.NewListCommand(loggingSyslogCmdRoot.CmdClause, g, m)
loggingSyslogUpdate := syslog.NewUpdateCommand(loggingSyslogCmdRoot.CmdClause, g, m)
popCmdRoot := pop.NewRootCommand(app, g)
productsCmdRoot := products.NewRootCommand(app, g, m)
profileCmdRoot := profile.NewRootCommand(app, g)
profileCreate := profile.NewCreateCommand(profileCmdRoot.CmdClause, profile.APIClientFactory(opts.APIClient), g)
profileDelete := profile.NewDeleteCommand(profileCmdRoot.CmdClause, g)
Expand Down Expand Up @@ -691,6 +693,7 @@ func defineCommands(
loggingSyslogList,
loggingSyslogUpdate,
popCmdRoot,
productsCmdRoot,
profileCmdRoot,
profileCreate,
profileDelete,
Expand Down
1 change: 1 addition & 0 deletions pkg/app/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ kv-store-entry
log-tail
logging
pops
products
profile
purge
rate-limit
Expand Down
2 changes: 2 additions & 0 deletions pkg/commands/products/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package products contains commands to inspect and manipulate Fastly products.
package products
40 changes: 40 additions & 0 deletions pkg/commands/products/products_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package products_test

import (
"bytes"
"testing"

"github.com/fastly/go-fastly/v8/fastly"

"github.com/fastly/cli/pkg/app"
"github.com/fastly/cli/pkg/mock"
"github.com/fastly/cli/pkg/testutil"
)

func TestAllDatacenters(t *testing.T) {
var stdout bytes.Buffer
args := testutil.Args("pops --token 123")
api := mock.API{
AllDatacentersFn: func() ([]fastly.Datacenter, error) {
return []fastly.Datacenter{
{
Name: "Foobar",
Code: "FBR",
Group: "Bar",
Shield: "Baz",
Coordinates: fastly.Coordinates{
Latitude: 1,
Longtitude: 2,
X: 3,
Y: 4,
},
},
}, nil
},
}
opts := testutil.NewRunOpts(args, &stdout)
opts.APIClient = mock.APIClient(api)
err := app.Run(opts)
testutil.AssertNoError(t, err)
testutil.AssertString(t, "\nNAME CODE GROUP SHIELD COORDINATES\nFoobar FBR Bar Baz {Latitude:1 Longtitude:2 X:3 Y:4}\n", stdout.String())
}
167 changes: 167 additions & 0 deletions pkg/commands/products/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package products

import (
"fmt"
"io"

"github.com/fastly/go-fastly/v8/fastly"

"github.com/fastly/cli/pkg/cmd"
fsterr "github.com/fastly/cli/pkg/errors"
"github.com/fastly/cli/pkg/global"
"github.com/fastly/cli/pkg/manifest"
"github.com/fastly/cli/pkg/text"
)

// RootCommand is the parent command for all subcommands in this package.
// It should be installed under the primary root command.
type RootCommand struct {
cmd.Base
manifest manifest.Data

// Optional.
disableProduct string
enableProduct string
serviceName cmd.OptionalServiceNameID
}

// ProductEnablementOptions is a list of products that can be enabled/disabled.
var ProductEnablementOptions = []string{
"brotli_compression",
"domain_inspector",
"fanout",
"image_optimizer",
"origin_inspector",
"websockets",
}

// NewRootCommand returns a new command registered in the parent.
func NewRootCommand(parent cmd.Registerer, g *global.Data, m manifest.Data) *RootCommand {
var c RootCommand
c.Globals = g
c.manifest = m
c.CmdClause = parent.Command("products", "Enable, disable, and check the enablement status of products on your services")
c.CmdClause.Flag("enable", "Enable product").HintOptions(ProductEnablementOptions...).EnumVar(&c.enableProduct, ProductEnablementOptions...)
c.CmdClause.Flag("disable", "Disable product").HintOptions(ProductEnablementOptions...).EnumVar(&c.disableProduct, ProductEnablementOptions...)
c.RegisterFlag(cmd.StringFlagOpts{
Name: cmd.FlagServiceIDName,
Description: cmd.FlagServiceIDDesc,
Dst: &c.manifest.Flag.ServiceID,
Short: 's',
})
c.RegisterFlag(cmd.StringFlagOpts{
Action: c.serviceName.Set,
Name: cmd.FlagServiceName,
Description: cmd.FlagServiceDesc,
Dst: &c.serviceName.Value,
})
return &c
}

// Exec implements the command interface.
func (c *RootCommand) Exec(_ io.Reader, out io.Writer) error {
ac := c.Globals.APIClient

if c.enableProduct != "" && c.disableProduct != "" {
return fsterr.ErrInvalidProductEnablementFlagCombo
}

serviceID, _, _, err := cmd.ServiceID(c.serviceName, c.manifest, c.Globals.APIClient, c.Globals.ErrLog)
if err != nil {
return fmt.Errorf("failed to identify Service ID: %w", err)
}

if c.enableProduct != "" {
if _, err := ac.EnableProduct(&fastly.ProductEnablementInput{
ProductID: identifyProduct(c.enableProduct),
ServiceID: serviceID,
}); err != nil {
return fmt.Errorf("failed to enable product '%s': %w", c.enableProduct, err)
}
text.Success(out, "Successfully enabled product '%s'", c.enableProduct)
return nil
}

if c.disableProduct != "" {
if err := ac.DisableProduct(&fastly.ProductEnablementInput{
ProductID: identifyProduct(c.disableProduct),
ServiceID: serviceID,
}); err != nil {
return fmt.Errorf("failed to disable product '%s': %w", c.disableProduct, err)
}
text.Success(out, "Successfully disabled product '%s'", c.disableProduct)
return nil
}

// NOTE: The API returns a 400 if a product is not enabled.
// The API client returns an error if a non-2xx is returned from the API.

var brotliEnabled, diEnabled, fanoutEnabled, ioEnabled, oiEnabled, wsEnabled bool

if _, err = ac.GetProduct(&fastly.ProductEnablementInput{
ProductID: fastly.ProductBrotliCompression,
ServiceID: serviceID,
}); err == nil {
brotliEnabled = true
}
if _, err = ac.GetProduct(&fastly.ProductEnablementInput{
ProductID: fastly.ProductDomainInspector,
ServiceID: serviceID,
}); err == nil {
diEnabled = true
}
if _, err = ac.GetProduct(&fastly.ProductEnablementInput{
ProductID: fastly.ProductFanout,
ServiceID: serviceID,
}); err == nil {
fanoutEnabled = true
}
if _, err = ac.GetProduct(&fastly.ProductEnablementInput{
ProductID: fastly.ProductImageOptimizer,
ServiceID: serviceID,
}); err == nil {
ioEnabled = true
}
if _, err = ac.GetProduct(&fastly.ProductEnablementInput{
ProductID: fastly.ProductOriginInspector,
ServiceID: serviceID,
}); err == nil {
oiEnabled = true
}
if _, err = ac.GetProduct(&fastly.ProductEnablementInput{
ProductID: fastly.ProductWebSockets,
ServiceID: serviceID,
}); err == nil {
wsEnabled = true
}

t := text.NewTable(out)
t.AddHeader("PRODUCT", "ENABLED")
t.AddLine("Brotli Compression", brotliEnabled)
t.AddLine("Domain Inspector", diEnabled)
t.AddLine("Fanout", fanoutEnabled)
t.AddLine("Image Optimizer", ioEnabled)
t.AddLine("Origin Inspector", oiEnabled)
t.AddLine("Web Sockets", wsEnabled)
t.Print()
return nil
}

func identifyProduct(product string) fastly.Product {
switch product {
case "brotli_compression":
return fastly.ProductBrotliCompression
case "domain_inspector":
return fastly.ProductDomainInspector
case "fanout":
return fastly.ProductFanout
case "image_optimizer":
return fastly.ProductImageOptimizer
case "origin_inspector":
return fastly.ProductOriginInspector
case "websockets":
return fastly.ProductWebSockets
default:
return fastly.ProductUndefined
}
}
7 changes: 7 additions & 0 deletions pkg/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,10 @@ var ErrInvalidStdinFileDirCombo = RemediationError{
Inner: fmt.Errorf("invalid flag combination"),
Remediation: "Use only one of --stdin, --file or --dir.",
}

// ErrInvalidProductEnablementFlagCombo means the user omitted both the --all and --key
// flags and we need at least one of them.
var ErrInvalidProductEnablementFlagCombo = RemediationError{
Inner: fmt.Errorf("invalid flag combination: --enable and --disable"),
Remediation: "Use either --enable or --disable, not both.",
}
19 changes: 19 additions & 0 deletions pkg/mock/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,10 @@ type API struct {
GetConditionFn func(i *fastly.GetConditionInput) (*fastly.Condition, error)
ListConditionsFn func(i *fastly.ListConditionsInput) ([]*fastly.Condition, error)
UpdateConditionFn func(i *fastly.UpdateConditionInput) (*fastly.Condition, error)

GetProductFn func(i *fastly.ProductEnablementInput) (*fastly.ProductEnablement, error)
EnableProductFn func(i *fastly.ProductEnablementInput) (*fastly.ProductEnablement, error)
DisableProductFn func(i *fastly.ProductEnablementInput) error
}

// AllDatacenters implements Interface.
Expand Down Expand Up @@ -1931,3 +1935,18 @@ func (m API) ListConditions(i *fastly.ListConditionsInput) ([]*fastly.Condition,
func (m API) UpdateCondition(i *fastly.UpdateConditionInput) (*fastly.Condition, error) {
return m.UpdateConditionFn(i)
}

// GetProduct implements Interface.
func (m API) GetProduct(i *fastly.ProductEnablementInput) (*fastly.ProductEnablement, error) {
return m.GetProductFn(i)
}

// EnableProduct implements Interface.
func (m API) EnableProduct(i *fastly.ProductEnablementInput) (*fastly.ProductEnablement, error) {
return m.EnableProductFn(i)
}

// DisableProduct implements Interface.
func (m API) DisableProduct(i *fastly.ProductEnablementInput) error {
return m.DisableProductFn(i)
}

0 comments on commit 63e66a6

Please sign in to comment.