Skip to content
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

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

Draft
wants to merge 2 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
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ require (
)

require (
github.com/dnaeon/go-vcr v1.2.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/stretchr/testify v1.10.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)

Expand Down Expand Up @@ -77,3 +78,5 @@ require (
)

require 4d63.com/optional v0.2.0

replace github.com/fastly/go-fastly/v9 => github.com/kpfleming/go-fastly/v9 v9.12.1-0.20241217164724-8f6ebe66851a
10 changes: 6 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj6
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2 h1:S6Dco8FtAhEI/qkg/00H6RdEGC+MCy5GPiQ+xweNRFE=
github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc=
github.com/fastly/go-fastly/v9 v9.12.0 h1:NUR4l+3LrSCux91sgKduFV8d/eejoGpQNlHa0ftKrFo=
github.com/fastly/go-fastly/v9 v9.12.0/go.mod h1:5w2jgJBZqQEebOwM/rRg7wutAcpDTziiMYWb/6qdM7U=
github.com/fastly/kingpin v2.1.12-0.20191105091915-95d230a53780+incompatible h1:FhrXlfhgGCS+uc6YwyiFUt04alnjpoX7vgDKJxS6Qbk=
github.com/fastly/kingpin v2.1.12-0.20191105091915-95d230a53780+incompatible/go.mod h1:U8UynVoU1SQaqD2I4ZqgYd5lx3A1ipQYn4aSt2Y5h6c=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
Expand Down Expand Up @@ -67,6 +65,8 @@ github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgo
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kpfleming/go-fastly/v9 v9.12.1-0.20241217164724-8f6ebe66851a h1:4NkDjmddTs2qY+41phkmIqV07XX4vDUZhLuOfZJtcXo=
github.com/kpfleming/go-fastly/v9 v9.12.1-0.20241217164724-8f6ebe66851a/go.mod h1:rB3T7CBBYBw+/W4rpzmZPev8BbARin6vriirVCY0yaw=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
Expand All @@ -91,6 +91,7 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/nicksnyder/go-i18n v1.10.3 h1:0U60fnLBNrLBVt8vb8Q67yKNs+gykbQuLsIkiesJL+w=
github.com/nicksnyder/go-i18n v1.10.3/go.mod h1:hvLG5HTlZ4UfSuVLSRuX7JRUomIaoKQM19hm6f+no7o=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
Expand Down Expand Up @@ -128,8 +129,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4=
github.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg=
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y=
Expand Down Expand Up @@ -209,6 +210,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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 @@ -79,6 +79,7 @@ kv-store-entry
log-tail
logging
pops
product
products
profile
purge
Expand Down
12 changes: 12 additions & 0 deletions pkg/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ 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/product"
"github.com/fastly/cli/pkg/commands/product/bot_management"
"github.com/fastly/cli/pkg/commands/products"
"github.com/fastly/cli/pkg/commands/profile"
"github.com/fastly/cli/pkg/commands/purge"
Expand Down Expand Up @@ -356,6 +358,11 @@ func Define(
loggingSyslogList := syslog.NewListCommand(loggingSyslogCmdRoot.CmdClause, data)
loggingSyslogUpdate := syslog.NewUpdateCommand(loggingSyslogCmdRoot.CmdClause, data)
popCmdRoot := pop.NewRootCommand(app, data)
productCmdRoot := product.NewRootCommand(app, data)
productBotManagementCmdRoot := bot_management.NewRootCommand(productCmdRoot.CmdClause, data)
productBotManagementDisable := bot_management.NewDisableCommand(productBotManagementCmdRoot.CmdClause, data)
productBotManagementEnable := bot_management.NewEnableCommand(productBotManagementCmdRoot.CmdClause, data)
productBotManagementStatus := bot_management.NewStatusCommand(productBotManagementCmdRoot.CmdClause, data)
productsCmdRoot := products.NewRootCommand(app, data)
profileCmdRoot := profile.NewRootCommand(app, data)
profileCreate := profile.NewCreateCommand(profileCmdRoot.CmdClause, data, ssoCmdRoot)
Expand Down Expand Up @@ -735,6 +742,11 @@ func Define(
loggingSyslogList,
loggingSyslogUpdate,
popCmdRoot,
productCmdRoot,
productBotManagementCmdRoot,
productBotManagementDisable,
productBotManagementEnable,
productBotManagementStatus,
productsCmdRoot,
profileCmdRoot,
profileCreate,
Expand Down
77 changes: 77 additions & 0 deletions pkg/commands/product/bot_management/disable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package bot_management

Check failure on line 1 in pkg/commands/product/bot_management/disable.go

View workflow job for this annotation

GitHub Actions / lint

don't use an underscore in package name

import (
"io"

"github.com/fastly/go-fastly/v9/fastly"
"github.com/fastly/go-fastly/v9/fastly/products/bot_management"

"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/cli/pkg/text"
)

// DisableFn is a dependency-injection point for unit tests to provide
// a mock implementation of the API operation.
var DisableFn = func(client api.Interface, serviceID string) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably a Go idiom or just a style preference of my own from familiarity with the standard library, but i suggest DisableFunc and friends.

return bot_management.Disable(client.(*fastly.Client), serviceID)
}
kpfleming marked this conversation as resolved.
Show resolved Hide resolved

// DisableCommand calls the Fastly API to disable the product.
type DisableCommand struct {
argparser.Base
Manifest manifest.Data

serviceName argparser.OptionalServiceNameID
}

// NewDisableCommand returns a usable command registered under the parent.
func NewDisableCommand(parent argparser.Registerer, g *global.Data) *DisableCommand {
c := DisableCommand{
Base: argparser.Base{
Globals: g,
},
}
c.CmdClause = parent.Command("disable", "Disable the "+bot_management.ProductName+" product")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first, I was rather confused until I realized that bot_management.ProductName refers to github.com/fastly/go-fastly/v9/fastly/products/bot_management.ProductName and not to something in this package. Same for the rest of the bot_management.Things in pkg/commands/product/bot_management/*.go files.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was confused by that when I started working in Go; if you're in package A, and you've also imported a package named A, then qualified references like A.foo always refer to the imported package and never to the 'local' package. Now that I've gotten used to it I'm not bothered by it, but this sort of thing is what prompted the desire for import aliases in the other project :-)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might I suggest an alias is probably appropriate here? bot or botapi could work

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what got me down the road of using a 'generic' import alias in the other project; if there are 9 packages that all import their corresponding 'API' functionality from another module, they could use the same import alias, to keep things consistent for the reader.

What crosses my mind is that if I see bot.ProductName and ddos.ProductName I might think that those are different in some way, but if they are both api.ProductName I'll know right away that they represent the same concept. Granted, calling it <anything>.ProductName should also be sufficient to express the concept...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try it and see what feels best, I guess!


// Optional.
c.RegisterFlag(argparser.StringFlagOpts{
Name: argparser.FlagServiceIDName,
Description: argparser.FlagServiceIDDesc,
Dst: &g.Manifest.Flag.ServiceID,
Short: 's',
})
c.RegisterFlag(argparser.StringFlagOpts{
Action: c.serviceName.Set,
Name: argparser.FlagServiceName,
Description: argparser.FlagServiceNameDesc,
Dst: &c.serviceName.Value,
})
return &c
}

// Exec invokes the application logic for the command.
func (c *DisableCommand) Exec(_ io.Reader, out io.Writer) error {
serviceID, source, flag, err := argparser.ServiceID(c.serviceName, *c.Globals.Manifest, c.Globals.APIClient, c.Globals.ErrLog)
if err != nil {
c.Globals.ErrLog.Add(err)
return err
}

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

err = DisableFn(c.Globals.APIClient, serviceID)
if err != nil {
c.Globals.ErrLog.Add(err)
return err
}

text.Success(out,
"Disabled "+bot_management.ProductName+" on service %s", serviceID)

return nil
}
3 changes: 3 additions & 0 deletions pkg/commands/product/bot_management/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package bot_management contains commands to enable and disable the
// Fastly Bot Management product.
package bot_management

Check failure on line 3 in pkg/commands/product/bot_management/doc.go

View workflow job for this annotation

GitHub Actions / lint

don't use an underscore in package name
77 changes: 77 additions & 0 deletions pkg/commands/product/bot_management/enable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package bot_management

Check failure on line 1 in pkg/commands/product/bot_management/enable.go

View workflow job for this annotation

GitHub Actions / lint

don't use an underscore in package name

import (
"io"

"github.com/fastly/go-fastly/v9/fastly"
"github.com/fastly/go-fastly/v9/fastly/products/bot_management"

"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/cli/pkg/text"
)

// EnableFn is a dependency-injection point for unit tests to provide
// a mock implementation of the API operation.
var EnableFn = func(client api.Interface, serviceID string) (*bot_management.EnableOutput, error) {
return bot_management.Enable(client.(*fastly.Client), serviceID)
}

// EnableCommand calls the Fastly API to enable the product.
type EnableCommand struct {
argparser.Base
Manifest manifest.Data

serviceName argparser.OptionalServiceNameID
}

// NewEnableCommand returns a usable command registered under the parent.
func NewEnableCommand(parent argparser.Registerer, g *global.Data) *EnableCommand {
c := EnableCommand{
Base: argparser.Base{
Globals: g,
},
}
c.CmdClause = parent.Command("enable", "Enable the "+bot_management.ProductName+" product")

// Optional.
c.RegisterFlag(argparser.StringFlagOpts{
Name: argparser.FlagServiceIDName,
Description: argparser.FlagServiceIDDesc,
Dst: &g.Manifest.Flag.ServiceID,
Short: 's',
})
c.RegisterFlag(argparser.StringFlagOpts{
Action: c.serviceName.Set,
Name: argparser.FlagServiceName,
Description: argparser.FlagServiceNameDesc,
Dst: &c.serviceName.Value,
})
return &c
}

// Exec invokes the application logic for the command.
func (c *EnableCommand) Exec(_ io.Reader, out io.Writer) error {
serviceID, source, flag, err := argparser.ServiceID(c.serviceName, *c.Globals.Manifest, c.Globals.APIClient, c.Globals.ErrLog)
if err != nil {
c.Globals.ErrLog.Add(err)
return err
}

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

_, err = EnableFn(c.Globals.APIClient, serviceID)
if err != nil {
c.Globals.ErrLog.Add(err)
return err
}

text.Success(out,
"Enabled "+bot_management.ProductName+" on service %s", serviceID)

return nil
}
125 changes: 125 additions & 0 deletions pkg/commands/product/bot_management/product_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package bot_management_test

import (
"testing"

"github.com/fastly/go-fastly/v9/fastly"
"github.com/fastly/go-fastly/v9/fastly/products/bot_management"

"github.com/fastly/cli/pkg/api"
root "github.com/fastly/cli/pkg/commands/product"
sub "github.com/fastly/cli/pkg/commands/product/bot_management"
"github.com/fastly/cli/pkg/global"
"github.com/fastly/cli/pkg/testutil"
)

func TestProductEnablement(t *testing.T) {
scenarios := []testutil.CLIScenario{
{
Name: "validate missing Service ID: enable",
Args: "enable",
WantError: "error reading service: no service ID found",
},
{
Name: "validate missing Service ID: disable",
Args: "enable",
WantError: "error reading service: no service ID found",
},
{
Name: "validate missing Service ID: status",
Args: "enable",
WantError: "error reading service: no service ID found",
},
{
Name: "validate invalid json/verbose flag combo: status",
Args: "status --service-id 123 --json --verbose",
WantError: "invalid flag combination, --verbose and --json",
},
{
Name: "validate success for enabling product",
Setup: func(t *testing.T, scenario *testutil.CLIScenario, opts *global.Data) {
sub.EnableFn = func(_ api.Interface, _ string) (*bot_management.EnableOutput, error) {
return nil, nil
}
kpfleming marked this conversation as resolved.
Show resolved Hide resolved
},
Args: "enable --service-id 123",
WantOutput: "SUCCESS: Enabled " + bot_management.ProductName + " on service 123",
},
{
Name: "validate failure for enabling product",
Setup: func(t *testing.T, scenario *testutil.CLIScenario, opts *global.Data) {
sub.EnableFn = func(_ api.Interface, _ string) (*bot_management.EnableOutput, error) {
return nil, testutil.Err
}
},
Args: "enable --service-id 123",
WantError: "test error",
},
{
Name: "validate success for disabling product",
Setup: func(t *testing.T, scenario *testutil.CLIScenario, opts *global.Data) {
sub.DisableFn = func(_ api.Interface, _ string) error {
return nil
}
},
Args: "disable --service-id 123",
WantOutput: "SUCCESS: Disabled " + bot_management.ProductName + " on service 123",
},
{
Name: "validate failure for disabling product",
Setup: func(t *testing.T, scenario *testutil.CLIScenario, opts *global.Data) {
sub.DisableFn = func(_ api.Interface, _ string) error {
return testutil.Err
}
},
Args: "disable --service-id 123",
WantError: "test error",
},
{
Name: "validate regular status output for enabled product",
Setup: func(t *testing.T, scenario *testutil.CLIScenario, opts *global.Data) {
sub.GetFn = func(_ api.Interface, _ string) (*bot_management.EnableOutput, error) {
return nil, nil
}
},
Args: "status --service-id 123",
WantOutput: "INFO: " + bot_management.ProductName + " is enabled on service 123",
},
{
Name: "validate JSON status output for enabled product",
Setup: func(t *testing.T, scenario *testutil.CLIScenario, opts *global.Data) {
sub.GetFn = func(_ api.Interface, _ string) (*bot_management.EnableOutput, error) {
return nil, nil
}
},
Args: "status --service-id 123 --json",
WantOutput: "{\n \"enabled\": true\n}",
},
{
Name: "validate regular status output for disabled product",
Setup: func(t *testing.T, scenario *testutil.CLIScenario, opts *global.Data) {
sub.GetFn = func(_ api.Interface, _ string) (*bot_management.EnableOutput, error) {
// The API returns a 'Bad Request' error when the
// product has not been enabled on the service
return nil, &fastly.HTTPError{StatusCode: 400}
}
},
Args: "status --service-id 123",
WantOutput: "INFO: " + bot_management.ProductName + " is disabled on service 123",
},
{
Name: "validate JSON status output for disabled product",
Setup: func(t *testing.T, scenario *testutil.CLIScenario, opts *global.Data) {
sub.GetFn = func(_ api.Interface, _ string) (*bot_management.EnableOutput, error) {
// The API returns a 'Bad Request' error when the
// product has not been enabled on the service
return nil, &fastly.HTTPError{StatusCode: 400}
}
},
Args: "status --service-id 123 --json",
WantOutput: "{\n \"enabled\": false\n}",
},
}

testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName}, scenarios)
}
Loading
Loading