Skip to content

Commit f4def70

Browse files
committed
feat(product): Add 'product' commands for each Fastly product.
Adds 'enable', 'disable', and 'status' commands for each product previously supported by the 'products' command (which is now deprecated). Also adds 'configure' commands for products which support configuration (DDoS Protection and Next-Gen WAF).
1 parent de4efcd commit f4def70

11 files changed

Lines changed: 481 additions & 5 deletions

File tree

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,12 @@ require (
3838
)
3939

4040
require (
41+
github.com/dnaeon/go-vcr v1.2.0 // indirect
4142
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
4243
github.com/kr/pretty v0.3.1 // indirect
4344
github.com/otiai10/mint v1.6.3 // indirect
4445
github.com/rogpeppe/go-internal v1.11.0 // indirect
45-
github.com/stretchr/testify v1.9.0 // indirect
46+
github.com/stretchr/testify v1.10.0 // indirect
4647
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
4748
)
4849

@@ -78,3 +79,5 @@ require (
7879
)
7980

8081
require 4d63.com/optional v0.2.0
82+
83+
replace github.com/fastly/go-fastly/v9 => github.com/kpfleming/go-fastly/v9 v9.12.1-0.20241217164724-8f6ebe66851a

go.sum

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj6
2424
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
2525
github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2 h1:S6Dco8FtAhEI/qkg/00H6RdEGC+MCy5GPiQ+xweNRFE=
2626
github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc=
27-
github.com/fastly/go-fastly/v9 v9.12.0 h1:NUR4l+3LrSCux91sgKduFV8d/eejoGpQNlHa0ftKrFo=
28-
github.com/fastly/go-fastly/v9 v9.12.0/go.mod h1:5w2jgJBZqQEebOwM/rRg7wutAcpDTziiMYWb/6qdM7U=
2927
github.com/fastly/kingpin v2.1.12-0.20191105091915-95d230a53780+incompatible h1:FhrXlfhgGCS+uc6YwyiFUt04alnjpoX7vgDKJxS6Qbk=
3028
github.com/fastly/kingpin v2.1.12-0.20191105091915-95d230a53780+incompatible/go.mod h1:U8UynVoU1SQaqD2I4ZqgYd5lx3A1ipQYn4aSt2Y5h6c=
3129
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
@@ -67,6 +65,8 @@ github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgo
6765
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
6866
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
6967
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
68+
github.com/kpfleming/go-fastly/v9 v9.12.1-0.20241217164724-8f6ebe66851a h1:4NkDjmddTs2qY+41phkmIqV07XX4vDUZhLuOfZJtcXo=
69+
github.com/kpfleming/go-fastly/v9 v9.12.1-0.20241217164724-8f6ebe66851a/go.mod h1:rB3T7CBBYBw+/W4rpzmZPev8BbARin6vriirVCY0yaw=
7070
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
7171
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
7272
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -91,6 +91,7 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ
9191
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
9292
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
9393
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
94+
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
9495
github.com/nicksnyder/go-i18n v1.10.3 h1:0U60fnLBNrLBVt8vb8Q67yKNs+gykbQuLsIkiesJL+w=
9596
github.com/nicksnyder/go-i18n v1.10.3/go.mod h1:hvLG5HTlZ4UfSuVLSRuX7JRUomIaoKQM19hm6f+no7o=
9697
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
@@ -128,8 +129,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
128129
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
129130
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
130131
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
131-
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
132-
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
132+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
133+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
133134
github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4=
134135
github.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg=
135136
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y=
@@ -209,6 +210,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
209210
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
210211
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
211212
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
213+
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
212214
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
213215
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
214216
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

pkg/commands/commands.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ import (
5151
"github.com/fastly/cli/pkg/commands/logging/syslog"
5252
"github.com/fastly/cli/pkg/commands/logtail"
5353
"github.com/fastly/cli/pkg/commands/pop"
54+
"github.com/fastly/cli/pkg/commands/product"
55+
"github.com/fastly/cli/pkg/commands/product/bot_management"
5456
"github.com/fastly/cli/pkg/commands/products"
5557
"github.com/fastly/cli/pkg/commands/profile"
5658
"github.com/fastly/cli/pkg/commands/purge"
@@ -356,6 +358,11 @@ func Define(
356358
loggingSyslogList := syslog.NewListCommand(loggingSyslogCmdRoot.CmdClause, data)
357359
loggingSyslogUpdate := syslog.NewUpdateCommand(loggingSyslogCmdRoot.CmdClause, data)
358360
popCmdRoot := pop.NewRootCommand(app, data)
361+
productCmdRoot := product.NewRootCommand(app, data)
362+
productBotManagementCmdRoot := bot_management.NewRootCommand(productCmdRoot.CmdClause, data)
363+
productBotManagementDisable := bot_management.NewDisableCommand(productBotManagementCmdRoot.CmdClause, data)
364+
productBotManagementEnable := bot_management.NewEnableCommand(productBotManagementCmdRoot.CmdClause, data)
365+
productBotManagementStatus := bot_management.NewStatusCommand(productBotManagementCmdRoot.CmdClause, data)
359366
productsCmdRoot := products.NewRootCommand(app, data)
360367
profileCmdRoot := profile.NewRootCommand(app, data)
361368
profileCreate := profile.NewCreateCommand(profileCmdRoot.CmdClause, data, ssoCmdRoot)
@@ -735,6 +742,11 @@ func Define(
735742
loggingSyslogList,
736743
loggingSyslogUpdate,
737744
popCmdRoot,
745+
productCmdRoot,
746+
productBotManagementCmdRoot,
747+
productBotManagementDisable,
748+
productBotManagementEnable,
749+
productBotManagementStatus,
738750
productsCmdRoot,
739751
profileCmdRoot,
740752
profileCreate,
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package bot_management
2+
3+
import (
4+
"io"
5+
6+
"github.com/fastly/go-fastly/v9/fastly"
7+
"github.com/fastly/go-fastly/v9/fastly/products/bot_management"
8+
9+
"github.com/fastly/cli/pkg/api"
10+
"github.com/fastly/cli/pkg/argparser"
11+
"github.com/fastly/cli/pkg/global"
12+
"github.com/fastly/cli/pkg/manifest"
13+
"github.com/fastly/cli/pkg/text"
14+
)
15+
16+
// DisableFn is a dependency-injection point for unit tests to provide
17+
// a mock implementation of the API operation.
18+
var DisableFn = func(client api.Interface, serviceID string) error {
19+
return bot_management.Disable(client.(*fastly.Client), serviceID)
20+
}
21+
22+
// DisableCommand calls the Fastly API to disable the product.
23+
type DisableCommand struct {
24+
argparser.Base
25+
Manifest manifest.Data
26+
27+
serviceName argparser.OptionalServiceNameID
28+
}
29+
30+
// NewDisableCommand returns a usable command registered under the parent.
31+
func NewDisableCommand(parent argparser.Registerer, g *global.Data) *DisableCommand {
32+
c := DisableCommand{
33+
Base: argparser.Base{
34+
Globals: g,
35+
},
36+
}
37+
c.CmdClause = parent.Command("disable", "Disable the "+bot_management.ProductName+" product")
38+
39+
// Optional.
40+
c.RegisterFlag(argparser.StringFlagOpts{
41+
Name: argparser.FlagServiceIDName,
42+
Description: argparser.FlagServiceIDDesc,
43+
Dst: &g.Manifest.Flag.ServiceID,
44+
Short: 's',
45+
})
46+
c.RegisterFlag(argparser.StringFlagOpts{
47+
Action: c.serviceName.Set,
48+
Name: argparser.FlagServiceName,
49+
Description: argparser.FlagServiceNameDesc,
50+
Dst: &c.serviceName.Value,
51+
})
52+
return &c
53+
}
54+
55+
// Exec invokes the application logic for the command.
56+
func (c *DisableCommand) Exec(_ io.Reader, out io.Writer) error {
57+
serviceID, source, flag, err := argparser.ServiceID(c.serviceName, *c.Globals.Manifest, c.Globals.APIClient, c.Globals.ErrLog)
58+
if err != nil {
59+
c.Globals.ErrLog.Add(err)
60+
return err
61+
}
62+
63+
if c.Globals.Verbose() {
64+
argparser.DisplayServiceID(serviceID, flag, source, out)
65+
}
66+
67+
err = DisableFn(c.Globals.APIClient, serviceID)
68+
if err != nil {
69+
c.Globals.ErrLog.Add(err)
70+
return err
71+
}
72+
73+
text.Success(out,
74+
"Disabled "+bot_management.ProductName+" on service %s", serviceID)
75+
76+
return nil
77+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Package bot_management contains commands to enable and disable the
2+
// Fastly Bot Management product.
3+
package bot_management
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package bot_management
2+
3+
import (
4+
"io"
5+
6+
"github.com/fastly/go-fastly/v9/fastly"
7+
"github.com/fastly/go-fastly/v9/fastly/products/bot_management"
8+
9+
"github.com/fastly/cli/pkg/api"
10+
"github.com/fastly/cli/pkg/argparser"
11+
"github.com/fastly/cli/pkg/global"
12+
"github.com/fastly/cli/pkg/manifest"
13+
"github.com/fastly/cli/pkg/text"
14+
)
15+
16+
// EnableFn is a dependency-injection point for unit tests to provide
17+
// a mock implementation of the API operation.
18+
var EnableFn = func(client api.Interface, serviceID string) (*bot_management.EnableOutput, error) {
19+
return bot_management.Enable(client.(*fastly.Client), serviceID)
20+
}
21+
22+
// EnableCommand calls the Fastly API to enable the product.
23+
type EnableCommand struct {
24+
argparser.Base
25+
Manifest manifest.Data
26+
27+
serviceName argparser.OptionalServiceNameID
28+
}
29+
30+
// NewEnableCommand returns a usable command registered under the parent.
31+
func NewEnableCommand(parent argparser.Registerer, g *global.Data) *EnableCommand {
32+
c := EnableCommand{
33+
Base: argparser.Base{
34+
Globals: g,
35+
},
36+
}
37+
c.CmdClause = parent.Command("enable", "Enable the "+bot_management.ProductName+" product")
38+
39+
// Optional.
40+
c.RegisterFlag(argparser.StringFlagOpts{
41+
Name: argparser.FlagServiceIDName,
42+
Description: argparser.FlagServiceIDDesc,
43+
Dst: &g.Manifest.Flag.ServiceID,
44+
Short: 's',
45+
})
46+
c.RegisterFlag(argparser.StringFlagOpts{
47+
Action: c.serviceName.Set,
48+
Name: argparser.FlagServiceName,
49+
Description: argparser.FlagServiceNameDesc,
50+
Dst: &c.serviceName.Value,
51+
})
52+
return &c
53+
}
54+
55+
// Exec invokes the application logic for the command.
56+
func (c *EnableCommand) Exec(_ io.Reader, out io.Writer) error {
57+
serviceID, source, flag, err := argparser.ServiceID(c.serviceName, *c.Globals.Manifest, c.Globals.APIClient, c.Globals.ErrLog)
58+
if err != nil {
59+
c.Globals.ErrLog.Add(err)
60+
return err
61+
}
62+
63+
if c.Globals.Verbose() {
64+
argparser.DisplayServiceID(serviceID, flag, source, out)
65+
}
66+
67+
_, err = EnableFn(c.Globals.APIClient, serviceID)
68+
if err != nil {
69+
c.Globals.ErrLog.Add(err)
70+
return err
71+
}
72+
73+
text.Success(out,
74+
"Enabled "+bot_management.ProductName+" on service %s", serviceID)
75+
76+
return nil
77+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package bot_management_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/fastly/go-fastly/v9/fastly"
7+
"github.com/fastly/go-fastly/v9/fastly/products/bot_management"
8+
9+
"github.com/fastly/cli/pkg/api"
10+
root "github.com/fastly/cli/pkg/commands/product"
11+
sub "github.com/fastly/cli/pkg/commands/product/bot_management"
12+
"github.com/fastly/cli/pkg/global"
13+
"github.com/fastly/cli/pkg/testutil"
14+
)
15+
16+
func TestProductEnablement(t *testing.T) {
17+
scenarios := []testutil.CLIScenario{
18+
{
19+
Name: "validate missing Service ID: enable",
20+
Args: "enable",
21+
WantError: "error reading service: no service ID found",
22+
},
23+
{
24+
Name: "validate missing Service ID: disable",
25+
Args: "enable",
26+
WantError: "error reading service: no service ID found",
27+
},
28+
{
29+
Name: "validate missing Service ID: status",
30+
Args: "enable",
31+
WantError: "error reading service: no service ID found",
32+
},
33+
{
34+
Name: "validate invalid json/verbose flag combo: status",
35+
Args: "status --service-id 123 --json --verbose",
36+
WantError: "invalid flag combination, --verbose and --json",
37+
},
38+
{
39+
Name: "validate success for enabling product",
40+
Setup: func(t *testing.T, scenario *testutil.CLIScenario, opts *global.Data) {
41+
sub.EnableFn = func(_ api.Interface, _ string) (*bot_management.EnableOutput, error) {
42+
return nil, nil
43+
}
44+
},
45+
Args: "enable --service-id 123",
46+
WantOutput: "SUCCESS: Enabled " + bot_management.ProductName + " on service 123",
47+
},
48+
{
49+
Name: "validate failure for enabling product",
50+
Setup: func(t *testing.T, scenario *testutil.CLIScenario, opts *global.Data) {
51+
sub.EnableFn = func(_ api.Interface, _ string) (*bot_management.EnableOutput, error) {
52+
return nil, testutil.Err
53+
}
54+
},
55+
Args: "enable --service-id 123",
56+
WantError: "test error",
57+
},
58+
{
59+
Name: "validate success for disabling product",
60+
Setup: func(t *testing.T, scenario *testutil.CLIScenario, opts *global.Data) {
61+
sub.DisableFn = func(_ api.Interface, _ string) error {
62+
return nil
63+
}
64+
},
65+
Args: "disable --service-id 123",
66+
WantOutput: "SUCCESS: Disabled " + bot_management.ProductName + " on service 123",
67+
},
68+
{
69+
Name: "validate failure for disabling product",
70+
Setup: func(t *testing.T, scenario *testutil.CLIScenario, opts *global.Data) {
71+
sub.DisableFn = func(_ api.Interface, _ string) error {
72+
return testutil.Err
73+
}
74+
},
75+
Args: "disable --service-id 123",
76+
WantError: "test error",
77+
},
78+
{
79+
Name: "validate regular status output for enabled product",
80+
Setup: func(t *testing.T, scenario *testutil.CLIScenario, opts *global.Data) {
81+
sub.GetFn = func(_ api.Interface, _ string) (*bot_management.EnableOutput, error) {
82+
return nil, nil
83+
}
84+
},
85+
Args: "status --service-id 123",
86+
WantOutput: "INFO: " + bot_management.ProductName + " is enabled on service 123",
87+
},
88+
{
89+
Name: "validate JSON status output for enabled product",
90+
Setup: func(t *testing.T, scenario *testutil.CLIScenario, opts *global.Data) {
91+
sub.GetFn = func(_ api.Interface, _ string) (*bot_management.EnableOutput, error) {
92+
return nil, nil
93+
}
94+
},
95+
Args: "status --service-id 123 --json",
96+
WantOutput: "{\n \"enabled\": true\n}",
97+
},
98+
{
99+
Name: "validate regular status output for disabled product",
100+
Setup: func(t *testing.T, scenario *testutil.CLIScenario, opts *global.Data) {
101+
sub.GetFn = func(_ api.Interface, _ string) (*bot_management.EnableOutput, error) {
102+
// The API returns a 'Bad Request' error when the
103+
// product has not been enabled on the service
104+
return nil, &fastly.HTTPError{StatusCode: 400}
105+
}
106+
},
107+
Args: "status --service-id 123",
108+
WantOutput: "INFO: " + bot_management.ProductName + " is disabled on service 123",
109+
},
110+
{
111+
Name: "validate JSON status output for disabled product",
112+
Setup: func(t *testing.T, scenario *testutil.CLIScenario, opts *global.Data) {
113+
sub.GetFn = func(_ api.Interface, _ string) (*bot_management.EnableOutput, error) {
114+
// The API returns a 'Bad Request' error when the
115+
// product has not been enabled on the service
116+
return nil, &fastly.HTTPError{StatusCode: 400}
117+
}
118+
},
119+
Args: "status --service-id 123 --json",
120+
WantOutput: "{\n \"enabled\": false\n}",
121+
},
122+
}
123+
124+
testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName}, scenarios)
125+
}

0 commit comments

Comments
 (0)