From f4835ff99dd71b44b893b3d5279f2cfa7a7c7ebd Mon Sep 17 00:00:00 2001 From: Katsuyuki Omuro Date: Wed, 14 Jan 2026 16:19:57 +0900 Subject: [PATCH 1/4] Implement [setup.products] --- pkg/commands/compute/deploy.go | 31 ++ pkg/commands/compute/setup/products.go | 407 ++++++++++++++++++ pkg/manifest/setup.go | 100 ++++- .../testdata/fastly-viceroy-update.toml | 5 + 4 files changed, 542 insertions(+), 1 deletion(-) create mode 100644 pkg/commands/compute/setup/products.go diff --git a/pkg/commands/compute/deploy.go b/pkg/commands/compute/deploy.go index 773950951..961118cdd 100644 --- a/pkg/commands/compute/deploy.go +++ b/pkg/commands/compute/deploy.go @@ -817,6 +817,7 @@ type ServiceResources struct { objectStores *setup.KVStores kvStores *setup.KVStores secretStores *setup.SecretStores + products *setup.Products } // ConstructNewServiceResources instantiates multiple [setup] config resources for a @@ -887,6 +888,17 @@ func (c *DeployCommand) ConstructNewServiceResources( Stdin: in, Stdout: out, } + + sr.products = &setup.Products{ + APIClient: c.Globals.APIClient, + AcceptDefaults: c.Globals.Flags.AcceptDefaults, + NonInteractive: c.Globals.Flags.NonInteractive, + ServiceID: serviceID, + ServiceVersion: serviceVersion, + Setup: c.Globals.Manifest.File.Setup.Products, + Stdin: in, + Stdout: out, + } } // ConfigureServiceResources calls the .Predefined() and .Configure() methods @@ -941,6 +953,13 @@ func (c *DeployCommand) ConfigureServiceResources(sr ServiceResources, serviceID } } + if sr.products.Predefined() { + if err := sr.products.Configure(); err != nil { + errLogService(c.Globals.ErrLog, err, serviceID, serviceVersion) + return fmt.Errorf("error configuring service products: %w", err) + } + } + return nil } @@ -957,6 +976,7 @@ func (c *DeployCommand) CreateServiceResources( sr.objectStores.Spinner = spinner sr.kvStores.Spinner = spinner sr.secretStores.Spinner = spinner + sr.products.Spinner = spinner if err := sr.backends.Create(); err != nil { c.Globals.ErrLog.AddWithContext(err, map[string]any{ @@ -1013,6 +1033,17 @@ func (c *DeployCommand) CreateServiceResources( return err } + if err := sr.products.Create(); err != nil { + c.Globals.ErrLog.AddWithContext(err, map[string]any{ + "Accept defaults": c.Globals.Flags.AcceptDefaults, + "Auto-yes": c.Globals.Flags.AutoYes, + "Non-interactive": c.Globals.Flags.NonInteractive, + "Service ID": serviceID, + "Service Version": serviceVersion, + }) + return err + } + return nil } diff --git a/pkg/commands/compute/setup/products.go b/pkg/commands/compute/setup/products.go new file mode 100644 index 000000000..88d02598c --- /dev/null +++ b/pkg/commands/compute/setup/products.go @@ -0,0 +1,407 @@ +package setup + +import ( + "context" + "errors" + "fmt" + "io" + + "github.com/fastly/cli/pkg/api" + fsterrors "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/manifest" + "github.com/fastly/cli/pkg/text" + "github.com/fastly/go-fastly/v12/fastly" + "github.com/fastly/go-fastly/v12/fastly/products/apidiscovery" + "github.com/fastly/go-fastly/v12/fastly/products/botmanagement" + "github.com/fastly/go-fastly/v12/fastly/products/brotlicompression" + "github.com/fastly/go-fastly/v12/fastly/products/ddosprotection" + "github.com/fastly/go-fastly/v12/fastly/products/domaininspector" + "github.com/fastly/go-fastly/v12/fastly/products/fanout" + "github.com/fastly/go-fastly/v12/fastly/products/imageoptimizer" + "github.com/fastly/go-fastly/v12/fastly/products/logexplorerinsights" + "github.com/fastly/go-fastly/v12/fastly/products/ngwaf" + "github.com/fastly/go-fastly/v12/fastly/products/origininspector" + "github.com/fastly/go-fastly/v12/fastly/products/websockets" +) + +// Products represents the service state related to Products defined +// within the fastly.toml [setup] configuration. +// +// NOTE: It implements the setup.Interface interface. +type Products struct { + // Public + APIClient api.Interface + AcceptDefaults bool + NonInteractive bool + Spinner text.Spinner + ServiceID string + ServiceVersion int + Setup *manifest.SetupProducts + Stdin io.Reader + Stdout io.Writer + + // Private + required Product +} + +// Product represents the configuration parameters for creating a KV Store via +// the API client. +type Product struct { + ApiDiscovery *ProductSettingsEnable + BotManagement *ProductSettingsEnable + BrotliCompression *ProductSettingsEnable + DdosProtection *ProductSettingsEnable + DomainInspector *ProductSettingsEnable + Fanout *ProductSettingsEnable + ImageOptimizer *ProductSettingsEnable + LogExplorerInsights *ProductSettingsEnable + Ngwaf *ProductSettingsNgwaf + OriginInspector *ProductSettingsEnable + WebSockets *ProductSettingsEnable +} + +type ProductSettings interface { + Enabled() bool +} + +type ProductSettingsEnable struct { + Enable bool +} + +var _ ProductSettings = (*ProductSettingsEnable)(nil) + +func (p *ProductSettingsEnable) Enabled() bool { + return p != nil && p.Enable +} + +type ProductSettingsNgwaf struct { + ProductSettingsEnable + WorkspaceID string +} + +var _ ProductSettings = (*ProductSettingsNgwaf)(nil) + +func (p *ProductSettingsNgwaf) Enabled() bool { + if p == nil { + return false + } + return p.ProductSettingsEnable.Enabled() +} + +// Predefined indicates if the service resource has been specified within the +// fastly.toml file using a [setup] configuration block. +func (p *Products) Predefined() bool { + return p != nil && p.Setup != nil && p.Setup.AnyEnabled() +} + +// Configure prompts the user for specific values related to the service resource. +func (p *Products) Configure() error { + if !p.Predefined() { + return nil + } + + text.Info(p.Stdout, "The package code will attempt to enable the following products on the service.\n\n") + + type productSpec struct { + path string + title string + run func() error // validates, prints, assigns required + } + + specs := []productSpec{ + { + run: func() error { + return configureIfEnabled( + p.Stdout, + p.Setup.ApiDiscovery, + &p.required.ApiDiscovery, + apidiscovery.ProductName, + "setup.products."+apidiscovery.ProductID, + ) + }, + }, + { + run: func() error { + return configureIfEnabled( + p.Stdout, + p.Setup.BotManagement, + &p.required.BotManagement, + botmanagement.ProductName, + "setup.products."+botmanagement.ProductID, + ) + }, + }, + { + run: func() error { + return configureIfEnabled( + p.Stdout, + p.Setup.BrotliCompression, + &p.required.BrotliCompression, + brotlicompression.ProductName, + "setup.products."+brotlicompression.ProductID, + ) + }, + }, + { + run: func() error { + return configureIfEnabled( + p.Stdout, + p.Setup.DdosProtection, + &p.required.DdosProtection, + ddosprotection.ProductName, + "setup.products."+ddosprotection.ProductID, + ) + }, + }, + { + run: func() error { + return configureIfEnabled( + p.Stdout, + p.Setup.DomainInspector, + &p.required.DomainInspector, + domaininspector.ProductName, + "setup.products."+domaininspector.ProductID, + ) + }, + }, + { + run: func() error { + return configureIfEnabled( + p.Stdout, + p.Setup.Fanout, + &p.required.Fanout, + fanout.ProductName, + "setup.products."+fanout.ProductID, + ) + }, + }, + { + run: func() error { + return configureIfEnabled( + p.Stdout, + p.Setup.ImageOptimizer, + &p.required.ImageOptimizer, + imageoptimizer.ProductName, + "setup.products."+imageoptimizer.ProductID, + ) + }, + }, + { + run: func() error { + return configureIfEnabled( + p.Stdout, + p.Setup.LogExplorerInsights, + &p.required.LogExplorerInsights, + logexplorerinsights.ProductName, + "setup.products."+logexplorerinsights.ProductID, + ) + }, + }, + { + run: func() error { + return whenEnabledDo( + p.Stdout, + p.Setup.Ngwaf, + func(productSettingsEnable ProductSettingsEnable) error { + text.Output(p.Stdout, " %s", p.Setup.Ngwaf.WorkspaceID) + p.required.Ngwaf = &ProductSettingsNgwaf{ + ProductSettingsEnable: productSettingsEnable, + WorkspaceID: p.Setup.Ngwaf.WorkspaceID, + } + return nil + }, + ngwaf.ProductName, + "setup.products."+ngwaf.ProductID, + ) + }, + }, + { + run: func() error { + return configureIfEnabled( + p.Stdout, + p.Setup.OriginInspector, + &p.required.OriginInspector, + origininspector.ProductName, + "setup.products."+origininspector.ProductID, + ) + }, + }, + { + run: func() error { + return configureIfEnabled( + p.Stdout, + p.Setup.WebSockets, + &p.required.WebSockets, + websockets.ProductName, + "setup.products."+websockets.ProductID, + ) + }, + }, + } + + for _, spec := range specs { + if err := spec.run(); err != nil { + return err + } + } + + return nil +} + +func whenEnabledDo(w io.Writer, s manifest.SetupProduct, fn func(productSettingsEnable ProductSettingsEnable) error, label string, path string) error { + if s == nil || !s.Enabled() { + return nil + } + if err := s.Validate(); err != nil { + return fmt.Errorf("%s: %w", path, err) + } + text.Output(w, "%s", text.Bold(label)) + return fn(ProductSettingsEnable{Enable: true}) +} + +func configureIfEnabled(w io.Writer, setupProduct manifest.SetupProduct, product **ProductSettingsEnable, label string, path string) error { + return whenEnabledDo(w, setupProduct, func(productSettingsEnable ProductSettingsEnable) error { + *product = &productSettingsEnable + return nil + }, label, path) +} + +// Create calls the relevant API to create the service resource(s). +func (p *Products) Create() error { + if p.Spinner == nil { + return fsterrors.RemediationError{ + Inner: fmt.Errorf("internal logic error: no spinner configured for setup.Products"), + Remediation: fsterrors.BugRemediation, + } + } + + fc, ok := p.APIClient.(*fastly.Client) + if !ok { + return errors.New("failed to convert interface to a fastly client") + } + + type enableSpec struct { + id string + enabled func() bool + enable func(fc *fastly.Client, serviceID string) error + } + + specs := []enableSpec{ + { + id: apidiscovery.ProductID, + enabled: func() bool { return p.required.ApiDiscovery.Enabled() }, + enable: func(fc *fastly.Client, serviceID string) error { + _, err := apidiscovery.Enable(context.TODO(), fc, serviceID) + return err + }, + }, + { + id: botmanagement.ProductID, + enabled: func() bool { return p.required.BotManagement.Enabled() }, + enable: func(fc *fastly.Client, serviceID string) error { + _, err := botmanagement.Enable(context.TODO(), fc, serviceID) + return err + }, + }, + { + id: brotlicompression.ProductID, + enabled: func() bool { return p.required.BrotliCompression.Enabled() }, + enable: func(fc *fastly.Client, serviceID string) error { + _, err := brotlicompression.Enable(context.TODO(), fc, serviceID) + return err + }, + }, + { + id: ddosprotection.ProductID, + enabled: func() bool { return p.required.DdosProtection.Enabled() }, + enable: func(fc *fastly.Client, serviceID string) error { + _, err := ddosprotection.Enable(context.TODO(), fc, serviceID) + return err + }, + }, + { + id: domaininspector.ProductID, + enabled: func() bool { return p.required.DomainInspector.Enabled() }, + enable: func(fc *fastly.Client, serviceID string) error { + _, err := domaininspector.Enable(context.TODO(), fc, serviceID) + return err + }, + }, + { + id: fanout.ProductID, + enabled: func() bool { return p.required.Fanout.Enabled() }, + enable: func(fc *fastly.Client, serviceID string) error { + _, err := fanout.Enable(context.TODO(), fc, serviceID) + return err + }, + }, + { + id: imageoptimizer.ProductID, + enabled: func() bool { return p.required.ImageOptimizer.Enabled() }, + enable: func(fc *fastly.Client, serviceID string) error { + _, err := imageoptimizer.Enable(context.TODO(), fc, serviceID) + return err + }, + }, + { + id: logexplorerinsights.ProductID, + enabled: func() bool { return p.required.LogExplorerInsights.Enabled() }, + enable: func(fc *fastly.Client, serviceID string) error { + _, err := logexplorerinsights.Enable(context.TODO(), fc, serviceID) + return err + }, + }, + { + id: ngwaf.ProductID, + enabled: func() bool { return p.required.Ngwaf.Enabled() }, + enable: func(fc *fastly.Client, serviceID string) error { + _, err := ngwaf.Enable(context.TODO(), fc, serviceID, ngwaf.EnableInput{WorkspaceID: p.required.Ngwaf.WorkspaceID}) + return err + }, + }, + { + id: origininspector.ProductID, + enabled: func() bool { return p.required.OriginInspector.Enabled() }, + enable: func(fc *fastly.Client, serviceID string) error { + _, err := origininspector.Enable(context.TODO(), fc, serviceID) + return err + }, + }, + { + id: websockets.ProductID, + enabled: func() bool { return p.required.WebSockets.Enabled() }, + enable: func(fc *fastly.Client, serviceID string) error { + _, err := websockets.Enable(context.TODO(), fc, serviceID) + return err + }, + }, + } + + for _, s := range specs { + if err := p.enableRequiredProduct(fc, s.id, s.enabled(), s.enable); err != nil { + return err + } + } + return nil +} + +func (p *Products) enableRequiredProduct( + fc *fastly.Client, + productID string, + isEnabled bool, + enableFn func(*fastly.Client, string) error, +) error { + if !isEnabled { + return nil + } + + return p.Spinner.Process( + fmt.Sprintf("Enabling product '%s'...", productID), + func(_ *text.SpinnerWrapper) error { + if err := enableFn(fc, p.ServiceID); err != nil { + return fmt.Errorf("error enabling product [%s]: %w", productID, err) + } + return nil + }, + ) +} diff --git a/pkg/manifest/setup.go b/pkg/manifest/setup.go index 97e8eca4d..e78111a7e 100644 --- a/pkg/manifest/setup.go +++ b/pkg/manifest/setup.go @@ -1,5 +1,10 @@ package manifest +import ( + "fmt" + "strings" +) + // Setup represents a set of service configuration that works with the code in // the package. See https://www.fastly.com/documentation/reference/compute/fastly-toml. type Setup struct { @@ -9,6 +14,7 @@ type Setup struct { ObjectStores map[string]*SetupKVStore `toml:"object_stores,omitempty"` KVStores map[string]*SetupKVStore `toml:"kv_stores,omitempty"` SecretStores map[string]*SetupSecretStore `toml:"secret_stores,omitempty"` + Products *SetupProducts `toml:"products,omitempty"` } // Defined indicates if there is any [setup] configuration in the manifest. @@ -24,10 +30,18 @@ func (s Setup) Defined() bool { if len(s.Loggers) > 0 { defined = true } + if len(s.ObjectStores) > 0 { + defined = true + } if len(s.KVStores) > 0 { defined = true } - + if len(s.SecretStores) > 0 { + defined = true + } + if s.Products != nil && s.Products.AnyEnabled() { + defined = true + } return defined } @@ -81,3 +95,87 @@ type SetupSecretStoreEntry struct { // values are input during setup. Description string `toml:"description,omitempty"` } + +type SetupProducts struct { + ApiDiscovery *SetupProductEnable `toml:"api_discovery,omitempty"` + BotManagement *SetupProductEnable `toml:"bot_management,omitempty"` + BrotliCompression *SetupProductEnable `toml:"brotli_compression,omitempty"` + DdosProtection *SetupProductEnable `toml:"ddos_protection,omitempty"` + DomainInspector *SetupProductEnable `toml:"domain_inspector,omitempty"` + Fanout *SetupProductEnable `toml:"fanout,omitempty"` + ImageOptimizer *SetupProductEnable `toml:"image_optimizer,omitempty"` + LogExplorerInsights *SetupProductEnable `toml:"log_explorer_insights,omitempty"` + Ngwaf *SetupProductNgwaf `toml:"ngwaf,omitempty"` + OriginInspector *SetupProductEnable `toml:"origin_inspector,omitempty"` + WebSockets *SetupProductEnable `toml:"websockets,omitempty"` +} + +type SetupProduct interface { + Enabled() bool + Validate() error +} + +type SetupProductEnable struct { + Enable bool `toml:"enable,omitempty"` +} + +var _ SetupProduct = (*SetupProductEnable)(nil) + +func (p *SetupProductEnable) Enabled() bool { + return p != nil && p.Enable +} +func (p *SetupProductEnable) Validate() error { + return nil +} + +type SetupProductNgwaf struct { + SetupProductEnable + WorkspaceID string `toml:"workspace_id,omitempty"` +} + +var _ SetupProduct = (*SetupProductNgwaf)(nil) + +func (p *SetupProductNgwaf) Enabled() bool { + if p == nil { + return false + } + return p.SetupProductEnable.Enabled() +} +func (p *SetupProductNgwaf) Validate() error { + if p == nil || !p.Enable { + return nil + } + if strings.TrimSpace(p.WorkspaceID) == "" { + return fmt.Errorf("workspace_id is required when enable = true") + } + return nil +} + +func (p *SetupProducts) AllSettings() []SetupProduct { + if p == nil { + return nil + } + + return []SetupProduct{ + p.ApiDiscovery, + p.BotManagement, + p.BrotliCompression, + p.DdosProtection, + p.DomainInspector, + p.Fanout, + p.ImageOptimizer, + p.LogExplorerInsights, + p.Ngwaf, + p.OriginInspector, + p.WebSockets, + } +} + +func (p *SetupProducts) AnyEnabled() bool { + for _, s := range p.AllSettings() { + if s != nil && s.Enabled() { + return true + } + } + return false +} diff --git a/pkg/manifest/testdata/fastly-viceroy-update.toml b/pkg/manifest/testdata/fastly-viceroy-update.toml index 017a33869..b95e3785c 100644 --- a/pkg/manifest/testdata/fastly-viceroy-update.toml +++ b/pkg/manifest/testdata/fastly-viceroy-update.toml @@ -75,3 +75,8 @@ file = "/path/to/other/secret.json" [[local_server.secret_stores.store_two]] key = "fourth" env = "ENV_FOURTH" + +[setup] +[setup.products] +[setup.products.fanout] +enable = true From 8c72c4fa3e4adbe65f6c75f2745bb40ff35b277f Mon Sep 17 00:00:00 2001 From: Katsuyuki Omuro Date: Thu, 15 Jan 2026 11:09:18 +0900 Subject: [PATCH 2/4] Formatting / linting --- pkg/commands/compute/setup/products.go | 12 +++++------- pkg/manifest/setup.go | 6 ++++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/commands/compute/setup/products.go b/pkg/commands/compute/setup/products.go index 88d02598c..fb71785ee 100644 --- a/pkg/commands/compute/setup/products.go +++ b/pkg/commands/compute/setup/products.go @@ -47,7 +47,7 @@ type Products struct { // Product represents the configuration parameters for creating a KV Store via // the API client. type Product struct { - ApiDiscovery *ProductSettingsEnable + APIDiscovery *ProductSettingsEnable BotManagement *ProductSettingsEnable BrotliCompression *ProductSettingsEnable DdosProtection *ProductSettingsEnable @@ -103,9 +103,7 @@ func (p *Products) Configure() error { text.Info(p.Stdout, "The package code will attempt to enable the following products on the service.\n\n") type productSpec struct { - path string - title string - run func() error // validates, prints, assigns required + run func() error } specs := []productSpec{ @@ -113,8 +111,8 @@ func (p *Products) Configure() error { run: func() error { return configureIfEnabled( p.Stdout, - p.Setup.ApiDiscovery, - &p.required.ApiDiscovery, + p.Setup.APIDiscovery, + &p.required.APIDiscovery, apidiscovery.ProductName, "setup.products."+apidiscovery.ProductID, ) @@ -289,7 +287,7 @@ func (p *Products) Create() error { specs := []enableSpec{ { id: apidiscovery.ProductID, - enabled: func() bool { return p.required.ApiDiscovery.Enabled() }, + enabled: func() bool { return p.required.APIDiscovery.Enabled() }, enable: func(fc *fastly.Client, serviceID string) error { _, err := apidiscovery.Enable(context.TODO(), fc, serviceID) return err diff --git a/pkg/manifest/setup.go b/pkg/manifest/setup.go index e78111a7e..7a4245cf2 100644 --- a/pkg/manifest/setup.go +++ b/pkg/manifest/setup.go @@ -97,7 +97,7 @@ type SetupSecretStoreEntry struct { } type SetupProducts struct { - ApiDiscovery *SetupProductEnable `toml:"api_discovery,omitempty"` + APIDiscovery *SetupProductEnable `toml:"api_discovery,omitempty"` BotManagement *SetupProductEnable `toml:"bot_management,omitempty"` BrotliCompression *SetupProductEnable `toml:"brotli_compression,omitempty"` DdosProtection *SetupProductEnable `toml:"ddos_protection,omitempty"` @@ -124,6 +124,7 @@ var _ SetupProduct = (*SetupProductEnable)(nil) func (p *SetupProductEnable) Enabled() bool { return p != nil && p.Enable } + func (p *SetupProductEnable) Validate() error { return nil } @@ -141,6 +142,7 @@ func (p *SetupProductNgwaf) Enabled() bool { } return p.SetupProductEnable.Enabled() } + func (p *SetupProductNgwaf) Validate() error { if p == nil || !p.Enable { return nil @@ -157,7 +159,7 @@ func (p *SetupProducts) AllSettings() []SetupProduct { } return []SetupProduct{ - p.ApiDiscovery, + p.APIDiscovery, p.BotManagement, p.BrotliCompression, p.DdosProtection, From fb983838941fe6dc20b68adf2c128791bfdc382f Mon Sep 17 00:00:00 2001 From: Katsuyuki Omuro Date: Tue, 20 Jan 2026 18:08:43 +0900 Subject: [PATCH 3/4] Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fc0ba451..186a23469 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - feat(compute/hashfiles): remove hashsum subcommand ([#1608](https://github.com/fastly/cli/pull/1608)) - feat(commands/ngwaf/rules): add support for CRUD operations for NGWAF rules ([#1578](https://github.com/fastly/cli/pull/1605)) - feat(compute/deploy): added the `--no-default-domain` flag to allow for the skipping of automatic domain creation when deploying a Compute service([#1610](https://github.com/fastly/cli/pull/1610)) +- feat(compute/deploy): Apply \[setup.products] for enabling products during initial deploy ([#1617](https://github.com/fastly/cli/pull/1617)) ### Bug fixes: From 90a95d75732f75c784a4728770e64e4f1a75753f Mon Sep 17 00:00:00 2001 From: Katsuyuki Omuro Date: Fri, 23 Jan 2026 18:12:15 +0900 Subject: [PATCH 4/4] WIP --- pkg/commands/compute/setup/products.go | 522 +++++++++--------- .../compute/setup/products_create_test.go | 165 ++++++ pkg/commands/compute/setup/products_test.go | 23 + pkg/manifest/setup.go | 124 ++--- 4 files changed, 485 insertions(+), 349 deletions(-) create mode 100644 pkg/commands/compute/setup/products_create_test.go create mode 100644 pkg/commands/compute/setup/products_test.go diff --git a/pkg/commands/compute/setup/products.go b/pkg/commands/compute/setup/products.go index fb71785ee..f9466524e 100644 --- a/pkg/commands/compute/setup/products.go +++ b/pkg/commands/compute/setup/products.go @@ -5,6 +5,8 @@ import ( "errors" "fmt" "io" + "reflect" + "strings" "github.com/fastly/cli/pkg/api" fsterrors "github.com/fastly/cli/pkg/errors" @@ -41,365 +43,349 @@ type Products struct { Stdout io.Writer // Private - required Product + required ProductsMap } -// Product represents the configuration parameters for creating a KV Store via -// the API client. -type Product struct { - APIDiscovery *ProductSettingsEnable - BotManagement *ProductSettingsEnable - BrotliCompression *ProductSettingsEnable - DdosProtection *ProductSettingsEnable - DomainInspector *ProductSettingsEnable - Fanout *ProductSettingsEnable - ImageOptimizer *ProductSettingsEnable - LogExplorerInsights *ProductSettingsEnable - Ngwaf *ProductSettingsNgwaf - OriginInspector *ProductSettingsEnable - WebSockets *ProductSettingsEnable +// ProductsMap represents the configuration parameters for enabling specified products +// for a service +type ProductsMap struct { + APIDiscovery ProductSettings + BotManagement ProductSettings + BrotliCompression ProductSettings + DdosProtection ProductSettings + DomainInspector ProductSettings + Fanout ProductSettings + ImageOptimizer ProductSettings + LogExplorerInsights ProductSettings + Ngwaf ProductSettings + OriginInspector ProductSettings + WebSockets ProductSettings } type ProductSettings interface { Enabled() bool } -type ProductSettingsEnable struct { +type Product struct { Enable bool } -var _ ProductSettings = (*ProductSettingsEnable)(nil) +func NewProductEnabled() *Product { + return &Product{Enable: true} +} -func (p *ProductSettingsEnable) Enabled() bool { +var _ ProductSettings = (*Product)(nil) + +func (p *Product) Enabled() bool { return p != nil && p.Enable } -type ProductSettingsNgwaf struct { - ProductSettingsEnable +type ProductNgwaf struct { + Product WorkspaceID string } -var _ ProductSettings = (*ProductSettingsNgwaf)(nil) - -func (p *ProductSettingsNgwaf) Enabled() bool { - if p == nil { - return false +func NewProductNgWaf(workspaceID string) *ProductNgwaf { + return &ProductNgwaf{ + Product: *NewProductEnabled(), + WorkspaceID: workspaceID, } - return p.ProductSettingsEnable.Enabled() } -// Predefined indicates if the service resource has been specified within the -// fastly.toml file using a [setup] configuration block. -func (p *Products) Predefined() bool { - return p != nil && p.Setup != nil && p.Setup.AnyEnabled() -} +var _ ProductSettings = (*ProductNgwaf)(nil) -// Configure prompts the user for specific values related to the service resource. -func (p *Products) Configure() error { - if !p.Predefined() { - return nil - } - - text.Info(p.Stdout, "The package code will attempt to enable the following products on the service.\n\n") +type productsSpec struct { + id string + name string + getSetupProduct func(*manifest.SetupProducts) manifest.SetupProductSettings + configure func(io.Writer, *ProductsMap, manifest.SetupProductSettings) error + getConfiguredProduct func(*ProductsMap) ProductSettings + enable func(*fastly.Client, ProductSettings, string) error +} - type productSpec struct { - run func() error - } +var productsSpecs []productsSpec - specs := []productSpec{ +func init() { + productsSpecs = []productsSpec{ { - run: func() error { - return configureIfEnabled( - p.Stdout, - p.Setup.APIDiscovery, - &p.required.APIDiscovery, - apidiscovery.ProductName, - "setup.products."+apidiscovery.ProductID, - ) + id: apidiscovery.ProductID, + name: apidiscovery.ProductName, + getSetupProduct: func(setupProducts *manifest.SetupProducts) manifest.SetupProductSettings { + return setupProducts.APIDiscovery }, - }, - { - run: func() error { - return configureIfEnabled( - p.Stdout, - p.Setup.BotManagement, - &p.required.BotManagement, - botmanagement.ProductName, - "setup.products."+botmanagement.ProductID, - ) + configure: func(w io.Writer, p *ProductsMap, sp manifest.SetupProductSettings) error { + p.APIDiscovery = NewProductEnabled() + return nil }, - }, - { - run: func() error { - return configureIfEnabled( - p.Stdout, - p.Setup.BrotliCompression, - &p.required.BrotliCompression, - brotlicompression.ProductName, - "setup.products."+brotlicompression.ProductID, - ) + getConfiguredProduct: func(products *ProductsMap) ProductSettings { + return products.APIDiscovery }, - }, - { - run: func() error { - return configureIfEnabled( - p.Stdout, - p.Setup.DdosProtection, - &p.required.DdosProtection, - ddosprotection.ProductName, - "setup.products."+ddosprotection.ProductID, - ) + enable: func(fc *fastly.Client, product ProductSettings, serviceID string) error { + _, err := apidiscovery.Enable(context.TODO(), fc, serviceID) + return err }, }, { - run: func() error { - return configureIfEnabled( - p.Stdout, - p.Setup.DomainInspector, - &p.required.DomainInspector, - domaininspector.ProductName, - "setup.products."+domaininspector.ProductID, - ) + id: botmanagement.ProductID, + name: botmanagement.ProductName, + getSetupProduct: func(setupProducts *manifest.SetupProducts) manifest.SetupProductSettings { + return setupProducts.BotManagement }, - }, - { - run: func() error { - return configureIfEnabled( - p.Stdout, - p.Setup.Fanout, - &p.required.Fanout, - fanout.ProductName, - "setup.products."+fanout.ProductID, - ) + configure: func(w io.Writer, p *ProductsMap, sp manifest.SetupProductSettings) error { + p.BotManagement = NewProductEnabled() + return nil }, - }, - { - run: func() error { - return configureIfEnabled( - p.Stdout, - p.Setup.ImageOptimizer, - &p.required.ImageOptimizer, - imageoptimizer.ProductName, - "setup.products."+imageoptimizer.ProductID, - ) + getConfiguredProduct: func(products *ProductsMap) ProductSettings { + return products.BotManagement }, - }, - { - run: func() error { - return configureIfEnabled( - p.Stdout, - p.Setup.LogExplorerInsights, - &p.required.LogExplorerInsights, - logexplorerinsights.ProductName, - "setup.products."+logexplorerinsights.ProductID, - ) + enable: func(fc *fastly.Client, product ProductSettings, serviceID string) error { + _, err := botmanagement.Enable(context.TODO(), fc, serviceID) + return err }, }, { - run: func() error { - return whenEnabledDo( - p.Stdout, - p.Setup.Ngwaf, - func(productSettingsEnable ProductSettingsEnable) error { - text.Output(p.Stdout, " %s", p.Setup.Ngwaf.WorkspaceID) - p.required.Ngwaf = &ProductSettingsNgwaf{ - ProductSettingsEnable: productSettingsEnable, - WorkspaceID: p.Setup.Ngwaf.WorkspaceID, - } - return nil - }, - ngwaf.ProductName, - "setup.products."+ngwaf.ProductID, - ) + id: brotlicompression.ProductID, + name: brotlicompression.ProductName, + getSetupProduct: func(setupProducts *manifest.SetupProducts) manifest.SetupProductSettings { + return setupProducts.BrotliCompression }, - }, - { - run: func() error { - return configureIfEnabled( - p.Stdout, - p.Setup.OriginInspector, - &p.required.OriginInspector, - origininspector.ProductName, - "setup.products."+origininspector.ProductID, - ) + configure: func(w io.Writer, p *ProductsMap, sp manifest.SetupProductSettings) error { + p.BrotliCompression = NewProductEnabled() + return nil }, - }, - { - run: func() error { - return configureIfEnabled( - p.Stdout, - p.Setup.WebSockets, - &p.required.WebSockets, - websockets.ProductName, - "setup.products."+websockets.ProductID, - ) + getConfiguredProduct: func(products *ProductsMap) ProductSettings { + return products.BrotliCompression }, - }, - } - - for _, spec := range specs { - if err := spec.run(); err != nil { - return err - } - } - - return nil -} - -func whenEnabledDo(w io.Writer, s manifest.SetupProduct, fn func(productSettingsEnable ProductSettingsEnable) error, label string, path string) error { - if s == nil || !s.Enabled() { - return nil - } - if err := s.Validate(); err != nil { - return fmt.Errorf("%s: %w", path, err) - } - text.Output(w, "%s", text.Bold(label)) - return fn(ProductSettingsEnable{Enable: true}) -} - -func configureIfEnabled(w io.Writer, setupProduct manifest.SetupProduct, product **ProductSettingsEnable, label string, path string) error { - return whenEnabledDo(w, setupProduct, func(productSettingsEnable ProductSettingsEnable) error { - *product = &productSettingsEnable - return nil - }, label, path) -} - -// Create calls the relevant API to create the service resource(s). -func (p *Products) Create() error { - if p.Spinner == nil { - return fsterrors.RemediationError{ - Inner: fmt.Errorf("internal logic error: no spinner configured for setup.Products"), - Remediation: fsterrors.BugRemediation, - } - } - - fc, ok := p.APIClient.(*fastly.Client) - if !ok { - return errors.New("failed to convert interface to a fastly client") - } - - type enableSpec struct { - id string - enabled func() bool - enable func(fc *fastly.Client, serviceID string) error - } - - specs := []enableSpec{ - { - id: apidiscovery.ProductID, - enabled: func() bool { return p.required.APIDiscovery.Enabled() }, - enable: func(fc *fastly.Client, serviceID string) error { - _, err := apidiscovery.Enable(context.TODO(), fc, serviceID) + enable: func(fc *fastly.Client, product ProductSettings, serviceID string) error { + _, err := brotlicompression.Enable(context.TODO(), fc, serviceID) return err }, }, { - id: botmanagement.ProductID, - enabled: func() bool { return p.required.BotManagement.Enabled() }, - enable: func(fc *fastly.Client, serviceID string) error { - _, err := botmanagement.Enable(context.TODO(), fc, serviceID) - return err + id: ddosprotection.ProductID, + name: ddosprotection.ProductName, + getSetupProduct: func(setupProducts *manifest.SetupProducts) manifest.SetupProductSettings { + return setupProducts.DdosProtection }, - }, - { - id: brotlicompression.ProductID, - enabled: func() bool { return p.required.BrotliCompression.Enabled() }, - enable: func(fc *fastly.Client, serviceID string) error { - _, err := brotlicompression.Enable(context.TODO(), fc, serviceID) - return err + configure: func(w io.Writer, p *ProductsMap, sp manifest.SetupProductSettings) error { + p.DdosProtection = NewProductEnabled() + return nil }, - }, - { - id: ddosprotection.ProductID, - enabled: func() bool { return p.required.DdosProtection.Enabled() }, - enable: func(fc *fastly.Client, serviceID string) error { + getConfiguredProduct: func(products *ProductsMap) ProductSettings { + return products.DdosProtection + }, + enable: func(fc *fastly.Client, product ProductSettings, serviceID string) error { _, err := ddosprotection.Enable(context.TODO(), fc, serviceID) return err }, }, { - id: domaininspector.ProductID, - enabled: func() bool { return p.required.DomainInspector.Enabled() }, - enable: func(fc *fastly.Client, serviceID string) error { + id: domaininspector.ProductID, + name: domaininspector.ProductName, + getSetupProduct: func(setupProducts *manifest.SetupProducts) manifest.SetupProductSettings { + return setupProducts.DomainInspector + }, + configure: func(w io.Writer, p *ProductsMap, sp manifest.SetupProductSettings) error { + p.DomainInspector = NewProductEnabled() + return nil + }, + getConfiguredProduct: func(products *ProductsMap) ProductSettings { + return products.DomainInspector + }, + enable: func(fc *fastly.Client, product ProductSettings, serviceID string) error { _, err := domaininspector.Enable(context.TODO(), fc, serviceID) return err }, }, { - id: fanout.ProductID, - enabled: func() bool { return p.required.Fanout.Enabled() }, - enable: func(fc *fastly.Client, serviceID string) error { + id: fanout.ProductID, + name: fanout.ProductName, + getSetupProduct: func(setupProducts *manifest.SetupProducts) manifest.SetupProductSettings { + return setupProducts.Fanout + }, + configure: func(w io.Writer, p *ProductsMap, sp manifest.SetupProductSettings) error { + p.Fanout = NewProductEnabled() + return nil + }, + getConfiguredProduct: func(products *ProductsMap) ProductSettings { + return products.Fanout + }, + enable: func(fc *fastly.Client, product ProductSettings, serviceID string) error { _, err := fanout.Enable(context.TODO(), fc, serviceID) return err }, }, { - id: imageoptimizer.ProductID, - enabled: func() bool { return p.required.ImageOptimizer.Enabled() }, - enable: func(fc *fastly.Client, serviceID string) error { + id: imageoptimizer.ProductID, + name: imageoptimizer.ProductName, + getSetupProduct: func(setupProducts *manifest.SetupProducts) manifest.SetupProductSettings { + return setupProducts.ImageOptimizer + }, + configure: func(w io.Writer, p *ProductsMap, sp manifest.SetupProductSettings) error { + p.ImageOptimizer = NewProductEnabled() + return nil + }, + getConfiguredProduct: func(products *ProductsMap) ProductSettings { + return products.ImageOptimizer + }, + enable: func(fc *fastly.Client, product ProductSettings, serviceID string) error { _, err := imageoptimizer.Enable(context.TODO(), fc, serviceID) return err }, }, { - id: logexplorerinsights.ProductID, - enabled: func() bool { return p.required.LogExplorerInsights.Enabled() }, - enable: func(fc *fastly.Client, serviceID string) error { + id: logexplorerinsights.ProductID, + name: logexplorerinsights.ProductName, + getSetupProduct: func(setupProducts *manifest.SetupProducts) manifest.SetupProductSettings { + return setupProducts.LogExplorerInsights + }, + configure: func(w io.Writer, p *ProductsMap, sp manifest.SetupProductSettings) error { + p.LogExplorerInsights = NewProductEnabled() + return nil + }, + getConfiguredProduct: func(products *ProductsMap) ProductSettings { + return products.LogExplorerInsights + }, + enable: func(fc *fastly.Client, product ProductSettings, serviceID string) error { _, err := logexplorerinsights.Enable(context.TODO(), fc, serviceID) return err }, }, { - id: ngwaf.ProductID, - enabled: func() bool { return p.required.Ngwaf.Enabled() }, - enable: func(fc *fastly.Client, serviceID string) error { - _, err := ngwaf.Enable(context.TODO(), fc, serviceID, ngwaf.EnableInput{WorkspaceID: p.required.Ngwaf.WorkspaceID}) + id: ngwaf.ProductID, + name: ngwaf.ProductName, + getSetupProduct: func(setupProducts *manifest.SetupProducts) manifest.SetupProductSettings { + return setupProducts.Ngwaf + }, + configure: func(w io.Writer, p *ProductsMap, sp manifest.SetupProductSettings) error { + ngwafSetupProduct, ok := sp.(*manifest.SetupProductNgwaf) + if !ok { + return fmt.Errorf("unexpected: Incorrect type for setupProduct") + } + if strings.TrimSpace(ngwafSetupProduct.WorkspaceID) == "" { + return fmt.Errorf("workspace_id is required") + } + text.Output(w, " workspace_id: %s", ngwafSetupProduct.WorkspaceID) + p.Ngwaf = NewProductNgWaf(ngwafSetupProduct.WorkspaceID) + return nil + }, + getConfiguredProduct: func(products *ProductsMap) ProductSettings { + return products.Ngwaf + }, + enable: func(fc *fastly.Client, product ProductSettings, serviceID string) error { + ngwafProduct, ok := product.(*ProductNgwaf) + if !ok { + return fmt.Errorf("unexpected: Incorrect type for product") + } + _, err := ngwaf.Enable(context.TODO(), fc, serviceID, ngwaf.EnableInput{WorkspaceID: ngwafProduct.WorkspaceID}) return err }, }, { - id: origininspector.ProductID, - enabled: func() bool { return p.required.OriginInspector.Enabled() }, - enable: func(fc *fastly.Client, serviceID string) error { + id: origininspector.ProductID, + name: origininspector.ProductName, + getSetupProduct: func(setupProducts *manifest.SetupProducts) manifest.SetupProductSettings { + return setupProducts.OriginInspector + }, + configure: func(w io.Writer, p *ProductsMap, sp manifest.SetupProductSettings) error { + p.OriginInspector = NewProductEnabled() + return nil + }, + getConfiguredProduct: func(products *ProductsMap) ProductSettings { + return products.OriginInspector + }, + enable: func(fc *fastly.Client, product ProductSettings, serviceID string) error { _, err := origininspector.Enable(context.TODO(), fc, serviceID) return err }, }, { - id: websockets.ProductID, - enabled: func() bool { return p.required.WebSockets.Enabled() }, - enable: func(fc *fastly.Client, serviceID string) error { + id: websockets.ProductID, + name: websockets.ProductName, + getSetupProduct: func(setupProducts *manifest.SetupProducts) manifest.SetupProductSettings { + return setupProducts.WebSockets + }, + configure: func(w io.Writer, p *ProductsMap, sp manifest.SetupProductSettings) error { + p.WebSockets = NewProductEnabled() + return nil + }, + getConfiguredProduct: func(products *ProductsMap) ProductSettings { + return products.WebSockets + }, + enable: func(fc *fastly.Client, product ProductSettings, serviceID string) error { _, err := websockets.Enable(context.TODO(), fc, serviceID) return err }, }, } +} - for _, s := range specs { - if err := p.enableRequiredProduct(fc, s.id, s.enabled(), s.enable); err != nil { - return err +// Predefined indicates if the service resource has been specified within the +// fastly.toml file using a [setup] configuration block. +func (p *Products) Predefined() bool { + return p != nil && p.Setup != nil && p.Setup.AnyDefined() +} + +// Configure prompts the user for specific values related to the service resource. +func (p *Products) Configure() error { + text.Info(p.Stdout, "The package code will attempt to enable the following products on the service.\n") + + for _, spec := range productsSpecs { + product := normalizeIfacePtr(spec.getSetupProduct(p.Setup)) + if product == nil || !product.Enabled() { + continue + } + text.Output(p.Stdout, "%s", text.Bold(spec.name)) + if err := spec.configure(p.Stdout, &p.required, product); err != nil { + return fmt.Errorf("%s: %w", "setup.products."+spec.id, err) } } + return nil } -func (p *Products) enableRequiredProduct( - fc *fastly.Client, - productID string, - isEnabled bool, - enableFn func(*fastly.Client, string) error, -) error { - if !isEnabled { - return nil +// Create calls the relevant API to create the service resource(s). +func (p *Products) Create() error { + if p.Spinner == nil { + return fsterrors.RemediationError{ + Inner: fmt.Errorf("internal logic error: no spinner configured for setup.Products"), + Remediation: fsterrors.BugRemediation, + } } - return p.Spinner.Process( - fmt.Sprintf("Enabling product '%s'...", productID), - func(_ *text.SpinnerWrapper) error { - if err := enableFn(fc, p.ServiceID); err != nil { - return fmt.Errorf("error enabling product [%s]: %w", productID, err) - } - return nil - }, - ) + fc, ok := p.APIClient.(*fastly.Client) + if !ok { + return errors.New("failed to convert interface to a fastly client") + } + + for _, spec := range productsSpecs { + product := normalizeIfacePtr(spec.getConfiguredProduct(&p.required)) + if product == nil || !product.Enabled() { + continue + } + err := p.Spinner.Process( + fmt.Sprintf("Enabling product '%s'...", spec.id), + func(_ *text.SpinnerWrapper) error { + if err := spec.enable(fc, product, p.ServiceID); err != nil { + return fmt.Errorf("error enabling product [%s]: %w", spec.id, err) + } + return nil + }, + ) + if err != nil { + return err + } + } + return nil +} + +// normalizeIfacePtr converts an interface holding a typed-nil pointer into a real nil interface. +// Works for any interface type parameter I. +func normalizeIfacePtr[I any](v I) I { + rv := reflect.ValueOf(v) + if !rv.IsValid() || (rv.Kind() == reflect.Ptr && rv.IsNil()) { + var zero I + return zero + } + return v } diff --git a/pkg/commands/compute/setup/products_create_test.go b/pkg/commands/compute/setup/products_create_test.go new file mode 100644 index 000000000..54e3715c9 --- /dev/null +++ b/pkg/commands/compute/setup/products_create_test.go @@ -0,0 +1,165 @@ +package setup_test + +import ( + "bytes" + "errors" + "io" + "net/http" + "strings" + "testing" + + "github.com/fastly/cli/pkg/commands/compute/setup" + "github.com/fastly/cli/pkg/manifest" + "github.com/fastly/cli/pkg/testutil" + "github.com/fastly/go-fastly/v12/fastly" +) + +// TestProductsCreate tests the `Create` method of the `Products` struct. +func TestProductsCreate(t *testing.T) { + scenarios := []struct { + name string + setup *manifest.SetupProducts + mockHTTP *mockHTTPClient + expectedError string + expectedOutput string + expectedAPIPaths []string + }{ + { + name: "successfully enables a single product", + setup: &manifest.SetupProducts{ + APIDiscovery: &manifest.SetupProduct{ + Enable: true, + }, + }, + mockHTTP: &mockHTTPClient{ + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"id":"api_discovery","name":"API Discovery"}`)), + }, + }, + expectedOutput: "Enabling product 'api_discovery'...", + expectedAPIPaths: []string{"/service/123/product/api_discovery/enable"}, + }, + { + name: "successfully enables multiple products", + setup: &manifest.SetupProducts{ + APIDiscovery: &manifest.SetupProduct{ + Enable: true, + }, + OriginInspector: &manifest.SetupProduct{ + Enable: true, + }, + }, + mockHTTP: &mockHTTPClient{ + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"id":"some_product","name":"Some Product"}`)), + }, + }, + expectedAPIPaths: []string{ + "/service/123/product/api_discovery/enable", + "/service/123/product/origin_inspector/enable", + }, + }, + { + name: "handles API error when enabling a product", + setup: &manifest.SetupProducts{ + APIDiscovery: &manifest.SetupProduct{ + Enable: true, + }, + }, + mockHTTP: &mockHTTPClient{ + err: errors.New("api error"), + }, + expectedError: "error enabling product [api_discovery]: api error", + }, + { + name: "no API calls when no products are configured", + setup: &manifest.SetupProducts{}, + mockHTTP: &mockHTTPClient{ + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"id":"some_product","name":"Some Product"}`)), + }, + }, + expectedAPIPaths: []string{}, + }, + { + name: "successfully enables ngwaf with workspace id", + setup: &manifest.SetupProducts{ + Ngwaf: &manifest.SetupProductNgwaf{ + SetupProduct: manifest.SetupProduct{ + Enable: true, + }, + WorkspaceID: "w-123", + }, + }, + mockHTTP: &mockHTTPClient{ + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"id":"ngwaf","name":"Next-Gen WAF"}`)), + }, + }, + expectedOutput: "Enabling product 'ngwaf'...", + expectedAPIPaths: []string{"/service/123/product/ngwaf/enable"}, + }, + } + + for _, testcase := range scenarios { + t.Run(testcase.name, func(t *testing.T) { + apiClient, err := fastly.NewClient(testcase.mockHTTP) + if err != nil { + t.Fatal(err) + } + + var out bytes.Buffer + products := setup.Products{ + APIClient: apiClient, + ServiceID: "123", + Spinner: testutil.NewSpinner(&out), + Stdout: &out, + Setup: testcase.setup, + } + + err = products.Configure() + testutil.AssertNoError(t, err) + + err = products.Create() + + if testcase.expectedError != "" { + testutil.AssertErrorContains(t, err, testcase.expectedError) + } else { + testutil.AssertNoError(t, err) + if testcase.expectedOutput != "" { + testutil.AssertStringContains(t, out.String(), testcase.expectedOutput) + } + } + + if len(testcase.expectedAPIPaths) != len(testcase.mockHTTP.called) { + t.Errorf("expected %d API calls, but got %d", len(testcase.expectedAPIPaths), len(testcase.mockHTTP.called)) + } + + for i, path := range testcase.expectedAPIPaths { + if i >= len(testcase.mockHTTP.called) { + t.Errorf("expected API path %s to be called, but it was not", path) + continue + } + testutil.AssertStringContains(t, testcase.mockHTTP.called[i], path) + } + }) + } +} + +type mockHTTPClient struct { + response *http.Response + err error + called []string +} + +func (m *mockHTTPClient) Do(req *http.Request) (*http.Response, error) { + m.called = append(m.called, req.URL.Path) + if m.err != nil { + return nil, m.err + } + return m.response, nil +} diff --git a/pkg/commands/compute/setup/products_test.go b/pkg/commands/compute/setup/products_test.go new file mode 100644 index 000000000..101bf5d93 --- /dev/null +++ b/pkg/commands/compute/setup/products_test.go @@ -0,0 +1,23 @@ +package setup_test + +// +//import ( +// "testing" +// +// "github.com/fastly/cli/pkg/commands/compute/setup" +//) +// +//func TestProductsCreate(t *testing.T) { +// +// products := &setup.Products{ +// APIClient: c.Globals.APIClient, +// AcceptDefaults: false, +// NonInteractive: false, +// ServiceID: "serviceID", +// ServiceVersion: 0, +// Setup: , +// Stdin: , +// Stdout: , +// } +// +//} diff --git a/pkg/manifest/setup.go b/pkg/manifest/setup.go index 7a4245cf2..94398df97 100644 --- a/pkg/manifest/setup.go +++ b/pkg/manifest/setup.go @@ -1,9 +1,6 @@ package manifest -import ( - "fmt" - "strings" -) +import "reflect" // Setup represents a set of service configuration that works with the code in // the package. See https://www.fastly.com/documentation/reference/compute/fastly-toml. @@ -30,16 +27,10 @@ func (s Setup) Defined() bool { if len(s.Loggers) > 0 { defined = true } - if len(s.ObjectStores) > 0 { - defined = true - } if len(s.KVStores) > 0 { defined = true } - if len(s.SecretStores) > 0 { - defined = true - } - if s.Products != nil && s.Products.AnyEnabled() { + if s.Products != nil && s.Products.AnyDefined() { defined = true } return defined @@ -97,87 +88,58 @@ type SetupSecretStoreEntry struct { } type SetupProducts struct { - APIDiscovery *SetupProductEnable `toml:"api_discovery,omitempty"` - BotManagement *SetupProductEnable `toml:"bot_management,omitempty"` - BrotliCompression *SetupProductEnable `toml:"brotli_compression,omitempty"` - DdosProtection *SetupProductEnable `toml:"ddos_protection,omitempty"` - DomainInspector *SetupProductEnable `toml:"domain_inspector,omitempty"` - Fanout *SetupProductEnable `toml:"fanout,omitempty"` - ImageOptimizer *SetupProductEnable `toml:"image_optimizer,omitempty"` - LogExplorerInsights *SetupProductEnable `toml:"log_explorer_insights,omitempty"` - Ngwaf *SetupProductNgwaf `toml:"ngwaf,omitempty"` - OriginInspector *SetupProductEnable `toml:"origin_inspector,omitempty"` - WebSockets *SetupProductEnable `toml:"websockets,omitempty"` -} - -type SetupProduct interface { - Enabled() bool - Validate() error -} - -type SetupProductEnable struct { - Enable bool `toml:"enable,omitempty"` -} + APIDiscovery *SetupProduct `toml:"api_discovery,omitempty"` + BotManagement *SetupProduct `toml:"bot_management,omitempty"` + BrotliCompression *SetupProduct `toml:"brotli_compression,omitempty"` + DdosProtection *SetupProduct `toml:"ddos_protection,omitempty"` + DomainInspector *SetupProduct `toml:"domain_inspector,omitempty"` + Fanout *SetupProduct `toml:"fanout,omitempty"` + ImageOptimizer *SetupProduct `toml:"image_optimizer,omitempty"` + LogExplorerInsights *SetupProduct `toml:"log_explorer_insights,omitempty"` + Ngwaf *SetupProductNgwaf `toml:"ngwaf,omitempty"` + OriginInspector *SetupProduct `toml:"origin_inspector,omitempty"` + WebSockets *SetupProduct `toml:"websockets,omitempty"` +} + +func (p *SetupProducts) AnyDefined() bool { + if p == nil { + return false + } -var _ SetupProduct = (*SetupProductEnable)(nil) + rv := reflect.ValueOf(p).Elem() // SetupProducts + settingsT := reflect.TypeOf((*SetupProductSettings)(nil)).Elem() -func (p *SetupProductEnable) Enabled() bool { - return p != nil && p.Enable -} + for i := 0; i < rv.NumField(); i++ { + fv := rv.Field(i) + if fv.Kind() != reflect.Ptr || fv.IsNil() { + continue + } -func (p *SetupProductEnable) Validate() error { - return nil -} + if fv.Type().Implements(settingsT) { + return true + } + } -type SetupProductNgwaf struct { - SetupProductEnable - WorkspaceID string `toml:"workspace_id,omitempty"` + return false } -var _ SetupProduct = (*SetupProductNgwaf)(nil) - -func (p *SetupProductNgwaf) Enabled() bool { - if p == nil { - return false - } - return p.SetupProductEnable.Enabled() +type SetupProductSettings interface { + Enabled() bool } -func (p *SetupProductNgwaf) Validate() error { - if p == nil || !p.Enable { - return nil - } - if strings.TrimSpace(p.WorkspaceID) == "" { - return fmt.Errorf("workspace_id is required when enable = true") - } - return nil +type SetupProduct struct { + Enable bool `toml:"enable,omitempty"` } -func (p *SetupProducts) AllSettings() []SetupProduct { - if p == nil { - return nil - } +var _ SetupProductSettings = (*SetupProduct)(nil) - return []SetupProduct{ - p.APIDiscovery, - p.BotManagement, - p.BrotliCompression, - p.DdosProtection, - p.DomainInspector, - p.Fanout, - p.ImageOptimizer, - p.LogExplorerInsights, - p.Ngwaf, - p.OriginInspector, - p.WebSockets, - } +func (p *SetupProduct) Enabled() bool { + return p != nil && p.Enable } -func (p *SetupProducts) AnyEnabled() bool { - for _, s := range p.AllSettings() { - if s != nil && s.Enabled() { - return true - } - } - return false +type SetupProductNgwaf struct { + SetupProduct + WorkspaceID string `toml:"workspace_id,omitempty"` } + +var _ SetupProductSettings = (*SetupProductNgwaf)(nil)