diff --git a/Makefile b/Makefile index 3f4a52550..60ae251f4 100644 --- a/Makefile +++ b/Makefile @@ -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 @@ -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 @@ -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 ./... @@ -105,13 +106,13 @@ semgrep: ## Run semgrep # To ignore lines use: //lint:ignore .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 diff --git a/internal/productcore/base.go b/internal/productcore/base.go new file mode 100644 index 000000000..871f26125 --- /dev/null +++ b/internal/productcore/base.go @@ -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) +} diff --git a/internal/productcore/disable.go b/internal/productcore/disable.go new file mode 100644 index 000000000..3659b050c --- /dev/null +++ b/internal/productcore/disable.go @@ -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 +} diff --git a/internal/productcore/doc.go b/internal/productcore/doc.go new file mode 100644 index 000000000..11b453c3b --- /dev/null +++ b/internal/productcore/doc.go @@ -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 diff --git a/internal/productcore/enable.go b/internal/productcore/enable.go new file mode 100644 index 000000000..6e47c4f39 --- /dev/null +++ b/internal/productcore/enable.go @@ -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 +} diff --git a/internal/productcore/status.go b/internal/productcore/status.go new file mode 100644 index 000000000..268e8d117 --- /dev/null +++ b/internal/productcore/status.go @@ -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 +} diff --git a/internal/productcore_test/doc.go b/internal/productcore_test/doc.go new file mode 100644 index 000000000..ef5ae533f --- /dev/null +++ b/internal/productcore_test/doc.go @@ -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 diff --git a/internal/productcore_test/enablement.go b/internal/productcore_test/enablement.go new file mode 100644 index 000000000..c0d84d709 --- /dev/null +++ b/internal/productcore_test/enablement.go @@ -0,0 +1,188 @@ +package productcore_test + +import ( + "strings" + "testing" + + "github.com/fastly/go-fastly/v9/fastly" + "github.com/fastly/go-fastly/v9/fastly/products" + + "github.com/fastly/cli/internal/productcore" + "github.com/fastly/cli/pkg/api" + "github.com/fastly/cli/pkg/global" + "github.com/fastly/cli/pkg/testutil" +) + +const TestServiceID = "123" + +type CommandRequiredArguments struct { + Command string + Arguments []string +} + +func MissingServiceIDScenarios(cra []CommandRequiredArguments) (scenarios []testutil.CLIScenario) { + for _, v := range cra { + scenario := testutil.CLIScenario{} + + scenario.Name = "validate missing Service ID: " + v.Command + scenario.Args = v.Command + for _, a := range v.Arguments { + if (!strings.HasPrefix(a, "--service-id")) { + scenario.Args += " " + a + } + } + scenario.WantError = "error reading service: no service ID found" + + scenarios = append(scenarios, scenario) + } + + return +} + +func InvalidJSONVerboseScenarios(cra []CommandRequiredArguments) (scenarios []testutil.CLIScenario) { + for _, v := range cra { + scenario := testutil.CLIScenario{} + + scenario.Name = "validate invalid json/verbose flag combo: " + v.Command + scenario.Args = v.Command + for _, a := range v.Arguments { + scenario.Args += " " + a + } + scenario.Args += " --json --verbose" + scenario.WantError = "invalid flag combination, --verbose and --json" + + scenarios = append(scenarios, scenario) + } + + return +} + +func EnableScenarios[O products.ProductOutput](cra []CommandRequiredArguments, productID, productName string, hooks *productcore.EnablementHookFuncs[O], mocker func(string) O) (scenarios []testutil.CLIScenario) { + for _, v := range cra { + if v.Command != "enable" { + continue + } + + scenario := testutil.CLIScenario{} + + scenario.Name = "validate text output success for enabling product" + scenario.Args = v.Command + for _, a := range v.Arguments { + scenario.Args += " " + a + } + scenario.Setup = func(_ *testing.T, _ *testutil.CLIScenario, _ *global.Data) { + hooks.EnableFunc = func(_ api.Interface, serviceID string) (O, error) { + return mocker(serviceID), nil + } + } + scenario.WantOutput = "SUCCESS: " + productName + " is enabled on service " + TestServiceID + scenarios = append(scenarios, scenario) + + scenario.Name = "validate JSON output success for enabling product" + scenario.Args += " --json" + scenario.WantOutput = "{\n \"product_id\": \"" + productID + "\",\n \"service_id\": \"" + TestServiceID + "\",\n \"enabled\": true\n}" + scenarios = append(scenarios, scenario) + + scenario.Name = "validate failure for enabling product" + scenario.Setup = func(_ *testing.T, _ *testutil.CLIScenario, _ *global.Data) { + hooks.EnableFunc = func(_ api.Interface, serviceID string) (O, error) { + return mocker(serviceID), testutil.Err + } + } + scenario.WantOutput = "" + scenario.WantError = "test error" + scenarios = append(scenarios, scenario) + } + + return +} + +func DisableScenarios[O products.ProductOutput](cra []CommandRequiredArguments, productID, productName string, hooks *productcore.EnablementHookFuncs[O]) (scenarios []testutil.CLIScenario) { + for _, v := range cra { + if v.Command != "disable" { + continue + } + + scenario := testutil.CLIScenario{} + + scenario.Name = "validate text output success for disabling product" + scenario.Args = v.Command + for _, a := range v.Arguments { + scenario.Args += " " + a + } + scenario.Setup = func(_ *testing.T, _ *testutil.CLIScenario, _ *global.Data) { + hooks.DisableFunc = func(_ api.Interface, serviceID string) error { + return nil + } + } + scenario.WantOutput = "SUCCESS: " + productName + " is disabled on service " + TestServiceID + scenarios = append(scenarios, scenario) + + scenario.Name = "validate JSON output success for disabling product" + scenario.Args += " --json" + scenario.WantOutput = "{\n \"product_id\": \"" + productID + "\",\n \"service_id\": \"" + TestServiceID + "\",\n \"enabled\": false\n}" + scenarios = append(scenarios, scenario) + + scenario.Name = "validate failure for disabling product" + scenario.Setup = func(_ *testing.T, _ *testutil.CLIScenario, _ *global.Data) { + hooks.DisableFunc = func(_ api.Interface, serviceID string) error { + return testutil.Err + } + } + scenario.WantOutput = "" + scenario.WantError = "test error" + scenarios = append(scenarios, scenario) + } + + return +} + +func StatusScenarios[O products.ProductOutput](cra []CommandRequiredArguments, productID, productName string, hooks *productcore.EnablementHookFuncs[O], mocker func(string) O) (scenarios []testutil.CLIScenario) { + for _, v := range cra { + if v.Command != "statu" { + continue + } + + scenario := testutil.CLIScenario{} + + scenario.Name = "validate text output for enabled product" + scenario.Args = v.Command + for _, a := range v.Arguments { + scenario.Args += " " + a + } + scenario.Setup = func(_ *testing.T, _ *testutil.CLIScenario, _ *global.Data) { + hooks.GetFunc = func(_ api.Interface, serviceID string) (O, error) { + return mocker(serviceID), nil + } + } + scenario.WantOutput = "INFO: " + productName + " is enabled on service " + TestServiceID + scenarios = append(scenarios, scenario) + + scenario.Name = "validate JSON output for enabled product" + scenario.Args += " --json" + scenario.WantOutput = "{\n \"product_id\": \"" + productID + "\",\n \"service_id\": \"" + TestServiceID + "\",\n \"enabled\": true\n}" + scenarios = append(scenarios, scenario) + + scenario.Name = "validate text output for disabled product" + scenario.Args = v.Command + for _, a := range v.Arguments { + scenario.Args += " " + a + } + scenario.Setup = func(_ *testing.T, _ *testutil.CLIScenario, _ *global.Data) { + hooks.GetFunc = func(_ api.Interface, serviceID string) (O, error) { + // The API returns a 'Bad Request' error when the + // product has not been enabled on the service + return mocker(serviceID), &fastly.HTTPError{StatusCode: 400} + } + } + scenario.WantOutput = "INFO: " + productName + " is disabled on service " + TestServiceID + scenarios = append(scenarios, scenario) + + scenario.Name = "validate JSON output for disabled product" + scenario.Args += " --json" + scenario.WantOutput = "{\n \"product_id\": \"" + productID + "\",\n \"service_id\": \"" + TestServiceID + "\",\n \"enabled\": false\n}" + scenarios = append(scenarios, scenario) + } + + return +} diff --git a/pkg/app/run_test.go b/pkg/app/run_test.go index 09100aa81..9e8426046 100644 --- a/pkg/app/run_test.go +++ b/pkg/app/run_test.go @@ -81,6 +81,7 @@ kv-store-entry log-tail logging pops +product products profile purge diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index 2d5e5be12..dbe573d11 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -55,6 +55,17 @@ 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/botmanagement" + + // "github.com/fastly/cli/pkg/commands/product/brotlicompression" + // "github.com/fastly/cli/pkg/commands/product/ddosprotection" + // "github.com/fastly/cli/pkg/commands/product/domaininspector" + // "github.com/fastly/cli/pkg/commands/product/fanout" + // "github.com/fastly/cli/pkg/commands/product/imageoptimizer" + // "github.com/fastly/cli/pkg/commands/product/logexplorerinsights" + // "github.com/fastly/cli/pkg/commands/product/origininspector" + // "github.com/fastly/cli/pkg/commands/product/websockets" "github.com/fastly/cli/pkg/commands/products" "github.com/fastly/cli/pkg/commands/profile" "github.com/fastly/cli/pkg/commands/purge" @@ -385,6 +396,53 @@ func Define( // nolint:revive // function-length loggingSyslogList := syslog.NewListCommand(loggingSyslogCmdRoot.CmdClause, data) loggingSyslogUpdate := syslog.NewUpdateCommand(loggingSyslogCmdRoot.CmdClause, data) popCmdRoot := pop.NewRootCommand(app, data) + + productCmdRoot := product.NewRootCommand(app, data) + productBotManagementCmdRoot := botmanagement.NewRootCommand(productCmdRoot.CmdClause, data) + productBotManagementDisable := botmanagement.NewDisableCommand(productBotManagementCmdRoot.CmdClause, data) + productBotManagementEnable := botmanagement.NewEnableCommand(productBotManagementCmdRoot.CmdClause, data) + productBotManagementStatus := botmanagement.NewStatusCommand(productBotManagementCmdRoot.CmdClause, data) + + // productBrotliCompressionCmdRoot := brotlicompression.NewRootCommand(productCmdRoot.CmdClause, data) + // productBrotliCompressionDisable := brotlicompression.NewDisableCommand(productBrotliCompressionCmdRoot.CmdClause, data) + // productBrotliCompressionEnable := brotlicompression.NewEnableCommand(productBrotliCompressionCmdRoot.CmdClause, data) + // productBrotliCompressionStatus := brotlicompression.NewStatusCommand(productBrotliCompressionCmdRoot.CmdClause, data) + + // productDDoSProtectionCmdRoot := ddosprotection.NewRootCommand(productCmdRoot.CmdClause, data) + // productDDoSProtectionDisable := ddosprotection.NewDisableCommand(productDDoSProtectionCmdRoot.CmdClause, data) + // productDDoSProtectionEnable := ddosprotection.NewEnableCommand(productDDoSProtectionCmdRoot.CmdClause, data) + // productDDoSProtectionStatus := ddosprotection.NewStatusCommand(productDDoSProtectionCmdRoot.CmdClause, data) + + // productDomainInspectorCmdRoot := domaininspector.NewRootCommand(productCmdRoot.CmdClause, data) + // productDomainInspectorDisable := domaininspector.NewDisableCommand(productDomainInspectorCmdRoot.CmdClause, data) + // productDomainInspectorEnable := domaininspector.NewEnableCommand(productDomainInspectorCmdRoot.CmdClause, data) + // productDomainInspectorStatus := domaininspector.NewStatusCommand(productDomainInspectorCmdRoot.CmdClause, data) + + // productFanoutCmdRoot := fanout.NewRootCommand(productCmdRoot.CmdClause, data) + // productFanoutDisable := fanout.NewDisableCommand(productFanoutCmdRoot.CmdClause, data) + // productFanoutEnable := fanout.NewEnableCommand(productFanoutCmdRoot.CmdClause, data) + // productFanoutStatus := fanout.NewStatusCommand(productFanoutCmdRoot.CmdClause, data) + + // productImageOptimizerCmdRoot := imageoptimizer.NewRootCommand(productCmdRoot.CmdClause, data) + // productImageOptimizerDisable := imageoptimizer.NewDisableCommand(productImageOptimizerCmdRoot.CmdClause, data) + // productImageOptimizerEnable := imageoptimizer.NewEnableCommand(productImageOptimizerCmdRoot.CmdClause, data) + // productImageOptimizerStatus := imageoptimizer.NewStatusCommand(productImageOptimizerCmdRoot.CmdClause, data) + + // productLogExplorerInsightsCmdRoot := logexplorerinsights.NewRootCommand(productCmdRoot.CmdClause, data) + // productLogExplorerInsightsDisable := logexplorerinsights.NewDisableCommand(productLogExplorerInsightsCmdRoot.CmdClause, data) + // productLogExplorerInsightsEnable := logexplorerinsights.NewEnableCommand(productLogExplorerInsightsCmdRoot.CmdClause, data) + // productLogExplorerInsightsStatus := logexplorerinsights.NewStatusCommand(productLogExplorerInsightsCmdRoot.CmdClause, data) + + // productOriginInspectorCmdRoot := origininspector.NewRootCommand(productCmdRoot.CmdClause, data) + // productOriginInspectorDisable := origininspector.NewDisableCommand(productOriginInspectorCmdRoot.CmdClause, data) + // productOriginInspectorEnable := origininspector.NewEnableCommand(productOriginInspectorCmdRoot.CmdClause, data) + // productOriginInspectorStatus := origininspector.NewStatusCommand(productOriginInspectorCmdRoot.CmdClause, data) + + // productWebSocketsCmdRoot := websockets.NewRootCommand(productCmdRoot.CmdClause, data) + // productWebSocketsDisable := websockets.NewDisableCommand(productWebSocketsCmdRoot.CmdClause, data) + // productWebSocketsEnable := websockets.NewEnableCommand(productWebSocketsCmdRoot.CmdClause, data) + // productWebSocketsStatus := websockets.NewStatusCommand(productWebSocketsCmdRoot.CmdClause, data) + productsCmdRoot := products.NewRootCommand(app, data) profileCmdRoot := profile.NewRootCommand(app, data) profileCreate := profile.NewCreateCommand(profileCmdRoot.CmdClause, data, ssoCmdRoot) @@ -789,6 +847,43 @@ func Define( // nolint:revive // function-length loggingSyslogList, loggingSyslogUpdate, popCmdRoot, + productCmdRoot, + productBotManagementCmdRoot, + productBotManagementDisable, + productBotManagementEnable, + productBotManagementStatus, + // productBrotliCompressionCmdRoot, + // productBrotliCompressionDisable, + // productBrotliCompressionEnable, + // productBrotliCompressionStatus, + // productDDoSProtectionCmdRoot, + // productDDoSProtectionDisable, + // productDDoSProtectionEnable, + // productDDoSProtectionStatus, + // productDomainInspectorCmdRoot, + // productDomainInspectorDisable, + // productDomainInspectorEnable, + // productDomainInspectorStatus, + // productFanoutCmdRoot, + // productFanoutDisable, + // productFanoutEnable, + // productFanoutStatus, + // productImageOptimizerCmdRoot, + // productImageOptimizerDisable, + // productImageOptimizerEnable, + // productImageOptimizerStatus, + // productLogExplorerInsightsCmdRoot, + // productLogExplorerInsightsDisable, + // productLogExplorerInsightsEnable, + // productLogExplorerInsightsStatus, + // productOriginInspectorCmdRoot, + // productOriginInspectorDisable, + // productOriginInspectorEnable, + // productOriginInspectorStatus, + // productWebSocketsCmdRoot, + // productWebSocketsDisable, + // productWebSocketsEnable, + // productWebSocketsStatus, productsCmdRoot, profileCmdRoot, profileCreate, diff --git a/pkg/commands/product/botmanagement/commands.go b/pkg/commands/product/botmanagement/commands.go new file mode 100644 index 000000000..ce5182667 --- /dev/null +++ b/pkg/commands/product/botmanagement/commands.go @@ -0,0 +1,104 @@ +package botmanagement + +import ( + "io" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/global" + "github.com/fastly/go-fastly/v9/fastly" + product "github.com/fastly/go-fastly/v9/fastly/products/botmanagement" + + "github.com/fastly/cli/internal/productcore" + "github.com/fastly/cli/pkg/api" +) + +// EnablementHooks is a structure of dependency-injection points used +// by unit tests to provide mock behaviors +var EnablementHooks = productcore.EnablementHookFuncs[product.EnableOutput]{ + DisableFunc: func(client api.Interface, serviceID string) error { + return product.Disable(client.(*fastly.Client), serviceID) + }, + EnableFunc: func(client api.Interface, serviceID string) (product.EnableOutput, error) { + return product.Enable(client.(*fastly.Client), serviceID) + }, + GetFunc: func(client api.Interface, serviceID string) (product.EnableOutput, error) { + return product.Get(client.(*fastly.Client), serviceID) + }, +} + +// RootCommand is the parent command for all subcommands in this package. +// It should be installed under the primary root command. +type RootCommand struct { + argparser.Base + // no flags +} + +// CommandName is the string to be used to invoke this command +const CommandName = "bot_management" + +// NewRootCommand returns a new command registered in the parent. +func NewRootCommand(parent argparser.Registerer, g *global.Data) *RootCommand { + var c RootCommand + c.Globals = g + c.CmdClause = parent.Command(CommandName, "Enable and disable the Bot Management product") + return &c +} + +// Exec implements the command interface. +func (c *RootCommand) Exec(_ io.Reader, _ io.Writer) error { + panic("unreachable") +} + +// EnableCommand calls the Fastly API to disable the product. +type EnableCommand struct { + productcore.Enable[product.EnableOutput, *productcore.EnablementStatus[product.EnableOutput]] +} + +// NewEnableCommand returns a usable command registered under the parent. +func NewEnableCommand(parent argparser.Registerer, g *global.Data) *EnableCommand { + c := EnableCommand{} + c.Init(parent, g, product.ProductName, &EnablementHooks) + return &c +} + +// Exec invokes the application logic for the command. +func (cmd *EnableCommand) Exec(_ io.Reader, out io.Writer) error { + status := &productcore.EnablementStatus[product.EnableOutput]{ProductName: product.ProductName} + return cmd.Enable.Exec(out, status) +} + +// DisableCommand calls the Fastly API to disable the product. +type DisableCommand struct { + productcore.Disable[product.EnableOutput, *productcore.EnablementStatus[product.EnableOutput]] +} + +// NewDisableCommand returns a usable command registered under the parent. +func NewDisableCommand(parent argparser.Registerer, g *global.Data) *DisableCommand { + c := DisableCommand{} + c.Init(parent, g, product.ProductID, product.ProductName, &EnablementHooks) + return &c +} + +// Exec invokes the application logic for the command. +func (cmd *DisableCommand) Exec(_ io.Reader, out io.Writer) error { + status := &productcore.EnablementStatus[product.EnableOutput]{ProductName: product.ProductName} + return cmd.Disable.Exec(out, status) +} + +// StatusCommand calls the Fastly API to get the enablement status of the product. +type StatusCommand struct { + productcore.Status[product.EnableOutput, *productcore.EnablementStatus[product.EnableOutput]] +} + +// NewStatusCommand returns a usable command registered under the parent. +func NewStatusCommand(parent argparser.Registerer, g *global.Data) *StatusCommand { + c := StatusCommand{} + c.Init(parent, g, product.ProductName, &EnablementHooks) + return &c +} + +// Exec invokes the application logic for the command. +func (cmd *StatusCommand) Exec(_ io.Reader, out io.Writer) error { + status := &productcore.EnablementStatus[product.EnableOutput]{ProductName: product.ProductName} + return cmd.Status.Exec(out, status) +} diff --git a/pkg/commands/product/botmanagement/commands_test.go b/pkg/commands/product/botmanagement/commands_test.go new file mode 100644 index 000000000..9560bb024 --- /dev/null +++ b/pkg/commands/product/botmanagement/commands_test.go @@ -0,0 +1,40 @@ +package botmanagement_test + +import ( + "testing" + + "github.com/fastly/cli/internal/productcore_test" + root "github.com/fastly/cli/pkg/commands/product" + sub "github.com/fastly/cli/pkg/commands/product/botmanagement" + product "github.com/fastly/go-fastly/v9/fastly/products/botmanagement" + "github.com/fastly/cli/pkg/testutil" +) + +var CRA = []productcore_test.CommandRequiredArguments{ + { + Command: "enable", + Arguments: []string{"--service-id " + productcore_test.TestServiceID}, + }, + { + Command: "disable", + Arguments: []string{"--service-id " + productcore_test.TestServiceID}, + }, + { + Command: "status", + Arguments: []string{"--service-id " + productcore_test.TestServiceID}, + }, +} + +func MockEnableOutput(serviceID string) product.EnableOutput { + return product.NewEnableOutput(serviceID) +} + +func TestBotManagementEnablement(t *testing.T) { + scenarios := productcore_test.MissingServiceIDScenarios(CRA) + scenarios = append(scenarios, productcore_test.InvalidJSONVerboseScenarios(CRA)...) + scenarios = append(scenarios, productcore_test.EnableScenarios(CRA, product.ProductID, product.ProductName, &sub.EnablementHooks, MockEnableOutput)...) + scenarios = append(scenarios, productcore_test.DisableScenarios(CRA, product.ProductID, product.ProductName, &sub.EnablementHooks)...) + scenarios = append(scenarios, productcore_test.StatusScenarios(CRA, product.ProductID, product.ProductName, &sub.EnablementHooks, MockEnableOutput)...) + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName}, scenarios) +} diff --git a/pkg/commands/product/botmanagement/doc.go b/pkg/commands/product/botmanagement/doc.go new file mode 100644 index 000000000..e735a6455 --- /dev/null +++ b/pkg/commands/product/botmanagement/doc.go @@ -0,0 +1,3 @@ +// Package botmanagement contains commands to enable and disable the +// Fastly Bot Management product. +package botmanagement diff --git a/pkg/commands/product/doc.go b/pkg/commands/product/doc.go new file mode 100644 index 000000000..e35f0ab32 --- /dev/null +++ b/pkg/commands/product/doc.go @@ -0,0 +1,3 @@ +// Package product contains commands to enable, disable, and configure +// Fastly products. +package product diff --git a/pkg/commands/product/root.go b/pkg/commands/product/root.go new file mode 100644 index 000000000..dcda72a2f --- /dev/null +++ b/pkg/commands/product/root.go @@ -0,0 +1,31 @@ +package product + +import ( + "io" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/global" +) + +// RootCommand is the parent command for all subcommands in this package. +// It should be installed under the primary root command. +type RootCommand struct { + argparser.Base + // no flags +} + +// CommandName is the string to be used to invoke this command +const CommandName = "product" + +// NewRootCommand returns a new command registered in the parent. +func NewRootCommand(parent argparser.Registerer, g *global.Data) *RootCommand { + var c RootCommand + c.Globals = g + c.CmdClause = parent.Command(CommandName, "Enable, disable, and configure Fastly products") + return &c +} + +// Exec implements the command interface. +func (c *RootCommand) Exec(_ io.Reader, _ io.Writer) error { + panic("unreachable") +} diff --git a/pkg/commands/products/root.go b/pkg/commands/products/root.go index 5a25cf487..f42c4f314 100644 --- a/pkg/commands/products/root.go +++ b/pkg/commands/products/root.go @@ -82,6 +82,7 @@ func (c *RootCommand) Exec(_ io.Reader, out io.Writer) error { } if c.enableProduct != "" { + text.Deprecated(out, "The 'products --enable=%s' command is deprecated and will be removed in a future release. Use 'product %s enable' instead.", c.enableProduct, c.enableProduct) p := identifyProduct(c.enableProduct) if p == fastly.ProductUndefined { return errors.New("unrecognised product") @@ -97,6 +98,7 @@ func (c *RootCommand) Exec(_ io.Reader, out io.Writer) error { } if c.disableProduct != "" { + text.Deprecated(out, "The 'products --disable=%s' command is deprecated and will be removed in a future release. Use 'product %s disable' instead.", c.disableProduct, c.disableProduct) p := identifyProduct(c.disableProduct) if p == fastly.ProductUndefined { return errors.New("unrecognised product") @@ -111,6 +113,8 @@ func (c *RootCommand) Exec(_ io.Reader, out io.Writer) error { return nil } + text.Deprecated(out, "The 'products' command is deprecated and will be removed in a future release. Use 'product status' for each product of interest.") + ps := ProductStatus{} if _, err = ac.GetProduct(&fastly.ProductEnablementInput{