From 7feaab9c7bc951e4f8790c78e1a4570ece358935 Mon Sep 17 00:00:00 2001 From: Integralist Date: Mon, 9 Oct 2023 10:14:39 +0100 Subject: [PATCH 1/9] build: bump go version to 1.21 --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 72735cede..33186dbc5 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/fastly/cli -go 1.20 +go 1.21 require ( github.com/Masterminds/semver/v3 v3.2.1 diff --git a/go.sum b/go.sum index 3e1d612d2..a1fb6563d 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= @@ -83,6 +84,7 @@ github.com/nwaples/rardecode v1.1.2/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWk github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= +github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= From 2b85f0e328d88b0638ebd72c27f94099f300f236 Mon Sep 17 00:00:00 2001 From: Integralist Date: Mon, 9 Oct 2023 14:21:47 +0100 Subject: [PATCH 2/9] doc(vcl/condition): fix typo --- pkg/commands/vcl/condition/create.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/commands/vcl/condition/create.go b/pkg/commands/vcl/condition/create.go index f0a0c0a66..feaddca18 100644 --- a/pkg/commands/vcl/condition/create.go +++ b/pkg/commands/vcl/condition/create.go @@ -3,12 +3,13 @@ package condition import ( "io" + "github.com/fastly/go-fastly/v8/fastly" + "github.com/fastly/cli/pkg/cmd" "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/global" "github.com/fastly/cli/pkg/manifest" "github.com/fastly/cli/pkg/text" - "github.com/fastly/go-fastly/v8/fastly" ) // ConditionTypes are the allowed input values for the --type flag. @@ -40,7 +41,7 @@ func NewCreateCommand(parent cmd.Registerer, g *global.Data, m manifest.Data) *C }, manifest: m, } - c.CmdClause = parent.Command("create", "Create a condtion on a Fastly service version").Alias("add") + c.CmdClause = parent.Command("create", "Create a condition on a Fastly service version").Alias("add") // Required flags c.RegisterFlag(cmd.StringFlagOpts{ From 63e66a6655f9ecd628f5f188ef5797c6c671c856 Mon Sep 17 00:00:00 2001 From: Integralist Date: Mon, 9 Oct 2023 16:57:53 +0100 Subject: [PATCH 3/9] feat(product_enablement): add products command --- pkg/api/interface.go | 4 + pkg/app/commands.go | 3 + pkg/app/run_test.go | 1 + pkg/commands/products/doc.go | 2 + pkg/commands/products/products_test.go | 40 ++++++ pkg/commands/products/root.go | 167 +++++++++++++++++++++++++ pkg/errors/errors.go | 7 ++ pkg/mock/api.go | 19 +++ 8 files changed, 243 insertions(+) create mode 100644 pkg/commands/products/doc.go create mode 100644 pkg/commands/products/products_test.go create mode 100644 pkg/commands/products/root.go diff --git a/pkg/api/interface.go b/pkg/api/interface.go index 1ac457681..08657b18e 100644 --- a/pkg/api/interface.go +++ b/pkg/api/interface.go @@ -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. diff --git a/pkg/app/commands.go b/pkg/app/commands.go index 2c05c8a30..5d7d78b8b 100644 --- a/pkg/app/commands.go +++ b/pkg/app/commands.go @@ -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" @@ -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) @@ -691,6 +693,7 @@ func defineCommands( loggingSyslogList, loggingSyslogUpdate, popCmdRoot, + productsCmdRoot, profileCmdRoot, profileCreate, profileDelete, diff --git a/pkg/app/run_test.go b/pkg/app/run_test.go index eb08a60ea..78b2fa7e2 100644 --- a/pkg/app/run_test.go +++ b/pkg/app/run_test.go @@ -75,6 +75,7 @@ kv-store-entry log-tail logging pops +products profile purge rate-limit diff --git a/pkg/commands/products/doc.go b/pkg/commands/products/doc.go new file mode 100644 index 000000000..944be33a9 --- /dev/null +++ b/pkg/commands/products/doc.go @@ -0,0 +1,2 @@ +// Package products contains commands to inspect and manipulate Fastly products. +package products diff --git a/pkg/commands/products/products_test.go b/pkg/commands/products/products_test.go new file mode 100644 index 000000000..8b55bd12f --- /dev/null +++ b/pkg/commands/products/products_test.go @@ -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()) +} diff --git a/pkg/commands/products/root.go b/pkg/commands/products/root.go new file mode 100644 index 000000000..f414fb130 --- /dev/null +++ b/pkg/commands/products/root.go @@ -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 + } +} diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index cd54bd9a1..981de877d 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -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.", +} diff --git a/pkg/mock/api.go b/pkg/mock/api.go index b36d33160..2f0344f80 100644 --- a/pkg/mock/api.go +++ b/pkg/mock/api.go @@ -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. @@ -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) +} From 27901b8d330f43ccd950310aeb20bc6a0214d4fd Mon Sep 17 00:00:00 2001 From: Integralist Date: Mon, 9 Oct 2023 17:24:41 +0100 Subject: [PATCH 4/9] test(product_enablement): add tests for products command --- pkg/commands/products/products_test.go | 110 +++++++++++++++++++------ pkg/commands/products/root.go | 3 - 2 files changed, 87 insertions(+), 26 deletions(-) diff --git a/pkg/commands/products/products_test.go b/pkg/commands/products/products_test.go index 8b55bd12f..02f2d5930 100644 --- a/pkg/commands/products/products_test.go +++ b/pkg/commands/products/products_test.go @@ -11,30 +11,94 @@ import ( "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, - }, +func TestProductEnablement(t *testing.T) { + args := testutil.Args + scenarios := []testutil.TestScenario{ + { + Name: "validate missing Service ID", + Args: args("products"), + WantError: "failed to identify Service ID: error reading service: no service ID found", + }, + { + Name: "validate invalid flag combo", + Args: args("products --enable fanout --disable fanout"), + WantError: "invalid flag combination: --enable and --disable", + }, + { + Name: "validate API error for product status", + API: mock.API{ + GetProductFn: func(i *fastly.ProductEnablementInput) (*fastly.ProductEnablement, error) { + return nil, testutil.Err + }, + }, + Args: args("products --service-id 123"), + WantOutput: `PRODUCT ENABLED +Brotli Compression false +Domain Inspector false +Fanout false +Image Optimizer false +Origin Inspector false +Web Sockets false +`, + }, + { + Name: "validate API success for product status", + API: mock.API{ + GetProductFn: func(i *fastly.ProductEnablementInput) (*fastly.ProductEnablement, error) { + return nil, nil + }, + }, + Args: args("products --service-id 123"), + WantOutput: `PRODUCT ENABLED +Brotli Compression true +Domain Inspector true +Fanout true +Image Optimizer true +Origin Inspector true +Web Sockets true +`, + }, + { + Name: "validate flag parsing error for enabling product", + Args: args("products --service-id 123 --enable foo"), + WantError: "error parsing arguments: enum value must be one of brotli_compression,domain_inspector,fanout,image_optimizer,origin_inspector,websockets, got 'foo'", + }, + { + Name: "validate flag parsing error for disabling product", + Args: args("products --service-id 123 --disable foo"), + WantError: "error parsing arguments: enum value must be one of brotli_compression,domain_inspector,fanout,image_optimizer,origin_inspector,websockets, got 'foo'", + }, + { + Name: "validate success for enabling product", + API: mock.API{ + EnableProductFn: func(i *fastly.ProductEnablementInput) (*fastly.ProductEnablement, error) { + return nil, nil + }, + }, + Args: args("products --service-id 123 --enable brotli_compression"), + WantOutput: "SUCCESS: Successfully enabled product 'brotli_compression'", + }, + { + Name: "validate success for disabling product", + API: mock.API{ + DisableProductFn: func(i *fastly.ProductEnablementInput) error { + return nil }, - }, nil + }, + Args: args("products --service-id 123 --disable brotli_compression"), + WantOutput: "SUCCESS: Successfully disabled product 'brotli_compression'", }, } - 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()) + + for testcaseIdx := range scenarios { + testcase := &scenarios[testcaseIdx] + t.Run(testcase.Name, func(t *testing.T) { + var stdout bytes.Buffer + opts := testutil.NewRunOpts(testcase.Args, &stdout) + opts.APIClient = mock.APIClient(testcase.API) + err := app.Run(opts) + testutil.AssertErrorContains(t, err, testcase.WantError) + testutil.AssertStringContains(t, stdout.String(), testcase.WantOutput) + }) + } } diff --git a/pkg/commands/products/root.go b/pkg/commands/products/root.go index f414fb130..578b61313 100644 --- a/pkg/commands/products/root.go +++ b/pkg/commands/products/root.go @@ -93,9 +93,6 @@ func (c *RootCommand) Exec(_ io.Reader, out io.Writer) error { 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{ From 8ce9a2e7cc0f8fc6ffdc536e00fcd05318c27107 Mon Sep 17 00:00:00 2001 From: Integralist Date: Mon, 9 Oct 2023 17:46:39 +0100 Subject: [PATCH 5/9] feat(product_enablement): support json output --- pkg/commands/products/products_test.go | 41 +++++++++++++++++++++++++- pkg/commands/products/root.go | 31 +++++++++++++++++-- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/pkg/commands/products/products_test.go b/pkg/commands/products/products_test.go index 02f2d5930..0d3a7ccbb 100644 --- a/pkg/commands/products/products_test.go +++ b/pkg/commands/products/products_test.go @@ -20,7 +20,7 @@ func TestProductEnablement(t *testing.T) { WantError: "failed to identify Service ID: error reading service: no service ID found", }, { - Name: "validate invalid flag combo", + Name: "validate invalid enable/disable flag combo", Args: args("products --enable fanout --disable fanout"), WantError: "invalid flag combination: --enable and --disable", }, @@ -88,6 +88,45 @@ Web Sockets true Args: args("products --service-id 123 --disable brotli_compression"), WantOutput: "SUCCESS: Successfully disabled product 'brotli_compression'", }, + { + Name: "validate invalid json/verbose flag combo", + Args: args("products --service-id 123 --json --verbose"), + WantError: "invalid flag combination, --verbose and --json", + }, + { + Name: "validate API error for product status with --json output", + API: mock.API{ + GetProductFn: func(i *fastly.ProductEnablementInput) (*fastly.ProductEnablement, error) { + return nil, testutil.Err + }, + }, + Args: args("products --service-id 123 --json"), + WantOutput: `{ + "brotli_compression": false, + "domain_inspector": false, + "fanout": false, + "image_optimizer": false, + "origin_inspector": false, + "websockets": false +}`, + }, + { + Name: "validate API success for product status with --json output", + API: mock.API{ + GetProductFn: func(i *fastly.ProductEnablementInput) (*fastly.ProductEnablement, error) { + return nil, nil + }, + }, + Args: args("products --service-id 123 --json"), + WantOutput: `{ + "brotli_compression": true, + "domain_inspector": true, + "fanout": true, + "image_optimizer": true, + "origin_inspector": true, + "websockets": true +}`, + }, } for testcaseIdx := range scenarios { diff --git a/pkg/commands/products/root.go b/pkg/commands/products/root.go index 578b61313..65dcc6e1e 100644 --- a/pkg/commands/products/root.go +++ b/pkg/commands/products/root.go @@ -17,11 +17,11 @@ import ( // It should be installed under the primary root command. type RootCommand struct { cmd.Base - manifest manifest.Data + cmd.JSONOutput - // Optional. disableProduct string enableProduct string + manifest manifest.Data serviceName cmd.OptionalServiceNameID } @@ -41,8 +41,11 @@ func NewRootCommand(parent cmd.Registerer, g *global.Data, m manifest.Data) *Roo 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...) + + // Optional. c.CmdClause.Flag("disable", "Disable product").HintOptions(ProductEnablementOptions...).EnumVar(&c.disableProduct, ProductEnablementOptions...) + c.CmdClause.Flag("enable", "Enable product").HintOptions(ProductEnablementOptions...).EnumVar(&c.enableProduct, ProductEnablementOptions...) + c.RegisterFlagBool(c.JSONFlag()) // --json c.RegisterFlag(cmd.StringFlagOpts{ Name: cmd.FlagServiceIDName, Description: cmd.FlagServiceIDDesc, @@ -66,6 +69,10 @@ func (c *RootCommand) Exec(_ io.Reader, out io.Writer) error { return fsterr.ErrInvalidProductEnablementFlagCombo } + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + 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) @@ -132,6 +139,24 @@ func (c *RootCommand) Exec(_ io.Reader, out io.Writer) error { wsEnabled = true } + if ok, err := c.WriteJSON(out, struct { + BrotliCompression bool `json:"brotli_compression"` + DomainInspector bool `json:"domain_inspector"` + Fanout bool `json:"fanout"` + ImageOptimizer bool `json:"image_optimizer"` + OriginInspector bool `json:"origin_inspector"` + WebSockets bool `json:"websockets"` + }{ + BrotliCompression: brotliEnabled, + DomainInspector: brotliEnabled, + Fanout: brotliEnabled, + ImageOptimizer: brotliEnabled, + OriginInspector: brotliEnabled, + WebSockets: brotliEnabled, + }); ok { + return err + } + t := text.NewTable(out) t.AddHeader("PRODUCT", "ENABLED") t.AddLine("Brotli Compression", brotliEnabled) From 28a1b240c8cf901277817b4ce61e1a1615198ae5 Mon Sep 17 00:00:00 2001 From: Mark McDonnell Date: Mon, 9 Oct 2023 17:59:22 +0100 Subject: [PATCH 6/9] Update pkg/commands/products/root.go Co-authored-by: Adam Williams --- pkg/commands/products/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/commands/products/root.go b/pkg/commands/products/root.go index 65dcc6e1e..659160a3c 100644 --- a/pkg/commands/products/root.go +++ b/pkg/commands/products/root.go @@ -40,7 +40,7 @@ func NewRootCommand(parent cmd.Registerer, g *global.Data, m manifest.Data) *Roo 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 = parent.Command("products", "Enable, disable, and check the enablement status of products") // Optional. c.CmdClause.Flag("disable", "Disable product").HintOptions(ProductEnablementOptions...).EnumVar(&c.disableProduct, ProductEnablementOptions...) From af48095a36bcd89d99232a0c6d76c03c65f62b61 Mon Sep 17 00:00:00 2001 From: Integralist Date: Mon, 9 Oct 2023 18:03:00 +0100 Subject: [PATCH 7/9] fix(product_enablement): reference correct variables --- pkg/commands/products/root.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/commands/products/root.go b/pkg/commands/products/root.go index 659160a3c..01105992f 100644 --- a/pkg/commands/products/root.go +++ b/pkg/commands/products/root.go @@ -148,11 +148,11 @@ func (c *RootCommand) Exec(_ io.Reader, out io.Writer) error { WebSockets bool `json:"websockets"` }{ BrotliCompression: brotliEnabled, - DomainInspector: brotliEnabled, - Fanout: brotliEnabled, - ImageOptimizer: brotliEnabled, - OriginInspector: brotliEnabled, - WebSockets: brotliEnabled, + DomainInspector: diEnabled, + Fanout: fanoutEnabled, + ImageOptimizer: ioEnabled, + OriginInspector: oiEnabled, + WebSockets: wsEnabled, }); ok { return err } From a26f7558621ab0b5fed09444ad2dca8f9e060767 Mon Sep 17 00:00:00 2001 From: Integralist Date: Mon, 9 Oct 2023 18:22:18 +0100 Subject: [PATCH 8/9] fix(product_enablement): check for ProductUndefined --- pkg/commands/products/root.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pkg/commands/products/root.go b/pkg/commands/products/root.go index 01105992f..00dd0df7f 100644 --- a/pkg/commands/products/root.go +++ b/pkg/commands/products/root.go @@ -1,6 +1,7 @@ package products import ( + "errors" "fmt" "io" @@ -79,8 +80,12 @@ func (c *RootCommand) Exec(_ io.Reader, out io.Writer) error { } if c.enableProduct != "" { + p := identifyProduct(c.enableProduct) + if p == fastly.ProductUndefined { + return errors.New("unrecognised product") + } if _, err := ac.EnableProduct(&fastly.ProductEnablementInput{ - ProductID: identifyProduct(c.enableProduct), + ProductID: p, ServiceID: serviceID, }); err != nil { return fmt.Errorf("failed to enable product '%s': %w", c.enableProduct, err) @@ -90,8 +95,12 @@ func (c *RootCommand) Exec(_ io.Reader, out io.Writer) error { } if c.disableProduct != "" { + p := identifyProduct(c.disableProduct) + if p == fastly.ProductUndefined { + return errors.New("unrecognised product") + } if err := ac.DisableProduct(&fastly.ProductEnablementInput{ - ProductID: identifyProduct(c.disableProduct), + ProductID: p, ServiceID: serviceID, }); err != nil { return fmt.Errorf("failed to disable product '%s': %w", c.disableProduct, err) From 85e8f81d1d0768f942ff28acf4bac1295e915274 Mon Sep 17 00:00:00 2001 From: Integralist Date: Tue, 10 Oct 2023 09:52:03 +0100 Subject: [PATCH 9/9] refactor(product_enablement): define new type for inline struct --- pkg/commands/products/root.go | 52 ++++++++++++++++------------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/pkg/commands/products/root.go b/pkg/commands/products/root.go index 00dd0df7f..bf799d5e3 100644 --- a/pkg/commands/products/root.go +++ b/pkg/commands/products/root.go @@ -109,71 +109,57 @@ func (c *RootCommand) Exec(_ io.Reader, out io.Writer) error { return nil } - var brotliEnabled, diEnabled, fanoutEnabled, ioEnabled, oiEnabled, wsEnabled bool + ps := ProductStatus{} if _, err = ac.GetProduct(&fastly.ProductEnablementInput{ ProductID: fastly.ProductBrotliCompression, ServiceID: serviceID, }); err == nil { - brotliEnabled = true + ps.BrotliCompression = true } if _, err = ac.GetProduct(&fastly.ProductEnablementInput{ ProductID: fastly.ProductDomainInspector, ServiceID: serviceID, }); err == nil { - diEnabled = true + ps.DomainInspector = true } if _, err = ac.GetProduct(&fastly.ProductEnablementInput{ ProductID: fastly.ProductFanout, ServiceID: serviceID, }); err == nil { - fanoutEnabled = true + ps.Fanout = true } if _, err = ac.GetProduct(&fastly.ProductEnablementInput{ ProductID: fastly.ProductImageOptimizer, ServiceID: serviceID, }); err == nil { - ioEnabled = true + ps.ImageOptimizer = true } if _, err = ac.GetProduct(&fastly.ProductEnablementInput{ ProductID: fastly.ProductOriginInspector, ServiceID: serviceID, }); err == nil { - oiEnabled = true + ps.OriginInspector = true } if _, err = ac.GetProduct(&fastly.ProductEnablementInput{ ProductID: fastly.ProductWebSockets, ServiceID: serviceID, }); err == nil { - wsEnabled = true + ps.WebSockets = true } - if ok, err := c.WriteJSON(out, struct { - BrotliCompression bool `json:"brotli_compression"` - DomainInspector bool `json:"domain_inspector"` - Fanout bool `json:"fanout"` - ImageOptimizer bool `json:"image_optimizer"` - OriginInspector bool `json:"origin_inspector"` - WebSockets bool `json:"websockets"` - }{ - BrotliCompression: brotliEnabled, - DomainInspector: diEnabled, - Fanout: fanoutEnabled, - ImageOptimizer: ioEnabled, - OriginInspector: oiEnabled, - WebSockets: wsEnabled, - }); ok { + if ok, err := c.WriteJSON(out, ps); ok { return err } 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.AddLine("Brotli Compression", ps.BrotliCompression) + t.AddLine("Domain Inspector", ps.DomainInspector) + t.AddLine("Fanout", ps.Fanout) + t.AddLine("Image Optimizer", ps.ImageOptimizer) + t.AddLine("Origin Inspector", ps.OriginInspector) + t.AddLine("Web Sockets", ps.WebSockets) t.Print() return nil } @@ -196,3 +182,13 @@ func identifyProduct(product string) fastly.Product { return fastly.ProductUndefined } } + +// ProductStatus indicates the status for each product. +type ProductStatus struct { + BrotliCompression bool `json:"brotli_compression"` + DomainInspector bool `json:"domain_inspector"` + Fanout bool `json:"fanout"` + ImageOptimizer bool `json:"image_optimizer"` + OriginInspector bool `json:"origin_inspector"` + WebSockets bool `json:"websockets"` +}