From 667c85eaf0937928e124cbf8d980e9c70b3bc830 Mon Sep 17 00:00:00 2001 From: Anthony Gomez Date: Thu, 30 Jul 2026 11:47:03 -0400 Subject: [PATCH 1/5] feat(integration): add platform integrations command group Add `fastly integration` commands to manage platform integrations, covering create, update, delete, describe, list, and list-types operations for Datadog, Jira Issue, JSM, Opsgenie, and Splunk On-Call. --- CHANGELOG.md | 2 + pkg/api/interface.go | 7 + pkg/app/metadata.json | 82 ++++++ pkg/app/run_test.go | 1 + pkg/argparser/common.go | 10 + pkg/commands/commands.go | 46 ++++ pkg/commands/integration/datadog/create.go | 82 ++++++ .../integration/datadog/datadog_test.go | 138 ++++++++++ pkg/commands/integration/datadog/doc.go | 3 + pkg/commands/integration/datadog/root.go | 31 +++ pkg/commands/integration/datadog/update.go | 94 +++++++ pkg/commands/integration/delete.go | 67 +++++ pkg/commands/integration/describe.go | 60 +++++ pkg/commands/integration/doc.go | 5 + pkg/commands/integration/integration_test.go | 253 ++++++++++++++++++ pkg/commands/integration/jiraissue/create.go | 91 +++++++ pkg/commands/integration/jiraissue/doc.go | 3 + .../integration/jiraissue/jiraissue_test.go | 114 ++++++++ pkg/commands/integration/jiraissue/root.go | 31 +++ pkg/commands/integration/jiraissue/update.go | 103 +++++++ pkg/commands/integration/jsm/create.go | 77 ++++++ pkg/commands/integration/jsm/doc.go | 3 + pkg/commands/integration/jsm/jsm_test.go | 126 +++++++++ pkg/commands/integration/jsm/root.go | 31 +++ pkg/commands/integration/jsm/update.go | 89 ++++++ pkg/commands/integration/list.go | 89 ++++++ pkg/commands/integration/list_types.go | 60 +++++ pkg/commands/integration/opsgenie/create.go | 77 ++++++ pkg/commands/integration/opsgenie/doc.go | 3 + .../integration/opsgenie/opsgenie_test.go | 126 +++++++++ pkg/commands/integration/opsgenie/root.go | 31 +++ pkg/commands/integration/opsgenie/update.go | 89 ++++++ pkg/commands/integration/root.go | 31 +++ .../integration/splunkoncall/create.go | 77 ++++++ pkg/commands/integration/splunkoncall/doc.go | 3 + pkg/commands/integration/splunkoncall/root.go | 31 +++ .../splunkoncall/splunkoncall_test.go | 126 +++++++++ .../integration/splunkoncall/update.go | 89 ++++++ pkg/mock/api.go | 37 +++ pkg/text/integration.go | 84 ++++++ 40 files changed, 2502 insertions(+) create mode 100644 pkg/commands/integration/datadog/create.go create mode 100644 pkg/commands/integration/datadog/datadog_test.go create mode 100644 pkg/commands/integration/datadog/doc.go create mode 100644 pkg/commands/integration/datadog/root.go create mode 100644 pkg/commands/integration/datadog/update.go create mode 100644 pkg/commands/integration/delete.go create mode 100644 pkg/commands/integration/describe.go create mode 100644 pkg/commands/integration/doc.go create mode 100644 pkg/commands/integration/integration_test.go create mode 100644 pkg/commands/integration/jiraissue/create.go create mode 100644 pkg/commands/integration/jiraissue/doc.go create mode 100644 pkg/commands/integration/jiraissue/jiraissue_test.go create mode 100644 pkg/commands/integration/jiraissue/root.go create mode 100644 pkg/commands/integration/jiraissue/update.go create mode 100644 pkg/commands/integration/jsm/create.go create mode 100644 pkg/commands/integration/jsm/doc.go create mode 100644 pkg/commands/integration/jsm/jsm_test.go create mode 100644 pkg/commands/integration/jsm/root.go create mode 100644 pkg/commands/integration/jsm/update.go create mode 100644 pkg/commands/integration/list.go create mode 100644 pkg/commands/integration/list_types.go create mode 100644 pkg/commands/integration/opsgenie/create.go create mode 100644 pkg/commands/integration/opsgenie/doc.go create mode 100644 pkg/commands/integration/opsgenie/opsgenie_test.go create mode 100644 pkg/commands/integration/opsgenie/root.go create mode 100644 pkg/commands/integration/opsgenie/update.go create mode 100644 pkg/commands/integration/root.go create mode 100644 pkg/commands/integration/splunkoncall/create.go create mode 100644 pkg/commands/integration/splunkoncall/doc.go create mode 100644 pkg/commands/integration/splunkoncall/root.go create mode 100644 pkg/commands/integration/splunkoncall/splunkoncall_test.go create mode 100644 pkg/commands/integration/splunkoncall/update.go create mode 100644 pkg/text/integration.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 07e43b8f9..da4e4d04c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ ### Enhancements: +- feat(integrations): add platform integrations command group ([#1868](https://github.com/fastly/cli/pull/1868)) + ### Dependencies: - build(deps): `golang.org/x/crypto` from 0.53.0 to 0.54.0 ([#1847](https://github.com/fastly/cli/pull/1847)) - build(deps): `github.com/bodgit/sevenzip` from 1.6.4 to 1.6.5 ([#1847](https://github.com/fastly/cli/pull/1847)) diff --git a/pkg/api/interface.go b/pkg/api/interface.go index 52c243064..488255088 100644 --- a/pkg/api/interface.go +++ b/pkg/api/interface.go @@ -357,6 +357,13 @@ type Interface interface { UpdateServiceAuthorization(context.Context, *fastly.UpdateServiceAuthorizationInput) (*fastly.ServiceAuthorization, error) DeleteServiceAuthorization(context.Context, *fastly.DeleteServiceAuthorizationInput) error + SearchIntegrations(context.Context, *fastly.SearchIntegrationsInput) (*fastly.SearchIntegrationsResponse, error) + CreateIntegration(context.Context, *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) + GetIntegration(context.Context, *fastly.GetIntegrationInput) (*fastly.Integration, error) + UpdateIntegration(context.Context, *fastly.UpdateIntegrationInput) error + DeleteIntegration(context.Context, *fastly.DeleteIntegrationInput) error + GetIntegrationTypes(context.Context) (*[]fastly.IntegrationType, error) + CreateConfigStore(context.Context, *fastly.CreateConfigStoreInput) (*fastly.ConfigStore, error) DeleteConfigStore(context.Context, *fastly.DeleteConfigStoreInput) error GetConfigStore(context.Context, *fastly.GetConfigStoreInput) (*fastly.ConfigStore, error) diff --git a/pkg/app/metadata.json b/pkg/app/metadata.json index f7e365e42..a1278bb3b 100644 --- a/pkg/app/metadata.json +++ b/pkg/app/metadata.json @@ -2858,6 +2858,88 @@ ] } }, + "integration": { + "list": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#search-integrations" + ] + }, + "describe": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#get-an-integration" + ] + }, + "delete": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#delete-an-integration" + ] + }, + "list-types": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#get-integration-types" + ] + }, + "datadog": { + "create": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#create-an-integration" + ] + }, + "update": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#update-an-integration" + ] + } + }, + "jiraissue": { + "create": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#create-an-integration" + ] + }, + "update": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#update-an-integration" + ] + } + }, + "jsm": { + "create": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#create-an-integration" + ] + }, + "update": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#update-an-integration" + ] + } + }, + "opsgenie": { + "create": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#create-an-integration" + ] + }, + "update": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#update-an-integration" + ] + } + }, + "splunkoncall": { + "create": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#create-an-integration" + ] + }, + "update": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#update-an-integration" + ] + } + } + }, "kv-store": { "create": { "apis": [ diff --git a/pkg/app/run_test.go b/pkg/app/run_test.go index 6b6e19fb9..9fb352344 100644 --- a/pkg/app/run_test.go +++ b/pkg/app/run_test.go @@ -75,6 +75,7 @@ dashboard dns domain install +integration ip-list kv-store kv-store-entry diff --git a/pkg/argparser/common.go b/pkg/argparser/common.go index 5b84d2fee..198b2f9f2 100644 --- a/pkg/argparser/common.go +++ b/pkg/argparser/common.go @@ -65,3 +65,13 @@ func StoreIDFlag(dst *string) StringFlagOpts { Required: true, } } + +// IntegrationIDFlag returns an id flag definition for a notification integration. +func IntegrationIDFlag(dst *string) StringFlagOpts { + return StringFlagOpts{ + Name: "id", + Description: "Integration ID", + Dst: dst, + Required: true, + } +} diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index ceecb1136..99f559019 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -69,6 +69,12 @@ import ( dnszone "github.com/fastly/cli/pkg/commands/dns/zone" "github.com/fastly/cli/pkg/commands/domain" "github.com/fastly/cli/pkg/commands/install" + "github.com/fastly/cli/pkg/commands/integration" + integrationDatadog "github.com/fastly/cli/pkg/commands/integration/datadog" + integrationJiraIssue "github.com/fastly/cli/pkg/commands/integration/jiraissue" + integrationJSM "github.com/fastly/cli/pkg/commands/integration/jsm" + integrationOpsgenie "github.com/fastly/cli/pkg/commands/integration/opsgenie" + integrationSplunkOnCall "github.com/fastly/cli/pkg/commands/integration/splunkoncall" "github.com/fastly/cli/pkg/commands/ip" "github.com/fastly/cli/pkg/commands/kvstore" "github.com/fastly/cli/pkg/commands/kvstoreentry" @@ -308,6 +314,26 @@ func Define( // nolint:revive // function-length domainList := domain.NewListCommand(domainCmdRoot.CmdClause, data) domainUpdate := domain.NewUpdateCommand(domainCmdRoot.CmdClause, data) installRoot := install.NewRootCommand(app, data) + integrationRoot := integration.NewRootCommand(app, data) + integrationList := integration.NewListCommand(integrationRoot.CmdClause, data) + integrationDescribe := integration.NewDescribeCommand(integrationRoot.CmdClause, data) + integrationDelete := integration.NewDeleteCommand(integrationRoot.CmdClause, data) + integrationListTypes := integration.NewListTypesCommand(integrationRoot.CmdClause, data) + integrationDatadogRoot := integrationDatadog.NewRootCommand(integrationRoot.CmdClause, data) + integrationDatadogCreate := integrationDatadog.NewCreateCommand(integrationDatadogRoot.CmdClause, data) + integrationDatadogUpdate := integrationDatadog.NewUpdateCommand(integrationDatadogRoot.CmdClause, data) + integrationJiraIssueRoot := integrationJiraIssue.NewRootCommand(integrationRoot.CmdClause, data) + integrationJiraIssueCreate := integrationJiraIssue.NewCreateCommand(integrationJiraIssueRoot.CmdClause, data) + integrationJiraIssueUpdate := integrationJiraIssue.NewUpdateCommand(integrationJiraIssueRoot.CmdClause, data) + integrationJSMRoot := integrationJSM.NewRootCommand(integrationRoot.CmdClause, data) + integrationJSMCreate := integrationJSM.NewCreateCommand(integrationJSMRoot.CmdClause, data) + integrationJSMUpdate := integrationJSM.NewUpdateCommand(integrationJSMRoot.CmdClause, data) + integrationOpsgenieRoot := integrationOpsgenie.NewRootCommand(integrationRoot.CmdClause, data) + integrationOpsgenieCreate := integrationOpsgenie.NewCreateCommand(integrationOpsgenieRoot.CmdClause, data) + integrationOpsgenieUpdate := integrationOpsgenie.NewUpdateCommand(integrationOpsgenieRoot.CmdClause, data) + integrationSplunkOnCallRoot := integrationSplunkOnCall.NewRootCommand(integrationRoot.CmdClause, data) + integrationSplunkOnCallCreate := integrationSplunkOnCall.NewCreateCommand(integrationSplunkOnCallRoot.CmdClause, data) + integrationSplunkOnCallUpdate := integrationSplunkOnCall.NewUpdateCommand(integrationSplunkOnCallRoot.CmdClause, data) ipCmdRoot := ip.NewRootCommand(app, data) kvstoreCmdRoot := kvstore.NewRootCommand(app, data) kvstoreCreate := kvstore.NewCreateCommand(kvstoreCmdRoot.CmdClause, data) @@ -1200,6 +1226,26 @@ func Define( // nolint:revive // function-length domainList, domainUpdate, installRoot, + integrationRoot, + integrationList, + integrationDescribe, + integrationDelete, + integrationListTypes, + integrationDatadogRoot, + integrationDatadogCreate, + integrationDatadogUpdate, + integrationJiraIssueRoot, + integrationJiraIssueCreate, + integrationJiraIssueUpdate, + integrationJSMRoot, + integrationJSMCreate, + integrationJSMUpdate, + integrationOpsgenieRoot, + integrationOpsgenieCreate, + integrationOpsgenieUpdate, + integrationSplunkOnCallRoot, + integrationSplunkOnCallCreate, + integrationSplunkOnCallUpdate, ipCmdRoot, kvstoreCreate, kvstoreDelete, diff --git a/pkg/commands/integration/datadog/create.go b/pkg/commands/integration/datadog/create.go new file mode 100644 index 000000000..08b7d5e04 --- /dev/null +++ b/pkg/commands/integration/datadog/create.go @@ -0,0 +1,82 @@ +package datadog + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// CreateCommand calls the Fastly API to create a Datadog notification integration. +type CreateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + IntegrationName string + APIKey string + + // Optional. + Description argparser.OptionalString + Site argparser.OptionalString +} + +// NewCreateCommand returns a usable command registered under the parent. +func NewCreateCommand(parent argparser.Registerer, g *global.Data) *CreateCommand { + c := CreateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("create", "Create a Datadog notification integration").Alias("add") + + // Required. + c.CmdClause.Flag("name", "The name of the integration").Short('n').Required().StringVar(&c.IntegrationName) + c.CmdClause.Flag("api-key", "Datadog API key").Required().StringVar(&c.APIKey) + + // Optional. + c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) + c.CmdClause.Flag("site", "Datadog site, e.g. \"datadoghq.eu\" (defaults to the US site)").Action(c.Site.Set).StringVar(&c.Site.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *CreateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + config := fastly.DatadogConfig{APIKey: c.APIKey} + if c.Site.WasSet { + config.Site = c.Site.Value + } + + input := &fastly.CreateIntegrationInput{ + Name: &c.IntegrationName, + Type: fastly.ToPointer(fastly.IntegrationTypeDatadog), + Config: config.ToMap(), + } + if c.Description.WasSet { + input.Description = &c.Description.Value + } + + o, err := c.Globals.APIClient.CreateIntegration(context.TODO(), input) + if err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if ok, err := c.WriteJSON(out, o); ok { + return err + } + + text.Success(out, "Created Datadog integration '%s' (id: %s)", c.IntegrationName, fastly.ToValue(o.ID)) + return nil +} diff --git a/pkg/commands/integration/datadog/datadog_test.go b/pkg/commands/integration/datadog/datadog_test.go new file mode 100644 index 000000000..94ea97868 --- /dev/null +++ b/pkg/commands/integration/datadog/datadog_test.go @@ -0,0 +1,138 @@ +package datadog_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/fastly/go-fastly/v17/fastly" + + root "github.com/fastly/cli/pkg/commands/integration" + sub "github.com/fastly/cli/pkg/commands/integration/datadog" + fstfmt "github.com/fastly/cli/pkg/fmt" + "github.com/fastly/cli/pkg/mock" + "github.com/fastly/cli/pkg/testutil" +) + +func TestCreateCommand(t *testing.T) { + const ( + integrationName = "test123" + integrationID = "integration-id-123" + apiKey = "a1b2c3d4" + ) + + scenarios := []testutil.CLIScenario{ + { + Args: fmt.Sprintf("--api-key %s", apiKey), + WantError: "error parsing arguments: required flag --name not provided", + }, + { + Args: fmt.Sprintf("--name %s", integrationName), + WantError: "error parsing arguments: required flag --api-key not provided", + }, + { + Args: fmt.Sprintf("--name %s --api-key %s", integrationName, apiKey), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, _ *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return nil, errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: fmt.Sprintf("--name %s --api-key %s", integrationName, apiKey), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, i *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + if fastly.ToValue(i.Type) != fastly.IntegrationTypeDatadog { + return nil, fmt.Errorf("unexpected type: %s", fastly.ToValue(i.Type)) + } + if i.Config["apikey"] != apiKey { + return nil, fmt.Errorf("unexpected apikey: %s", i.Config["apikey"]) + } + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.Success("Created Datadog integration '%s' (id: %s)", integrationName, integrationID), + }, + { + Args: fmt.Sprintf("--name %s --api-key %s --site %s", integrationName, apiKey, "datadoghq.eu"), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, i *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + if i.Config["site"] != "datadoghq.eu" { + return nil, fmt.Errorf("unexpected site: %s", i.Config["site"]) + } + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.Success("Created Datadog integration '%s' (id: %s)", integrationName, integrationID), + }, + { + Args: fmt.Sprintf("--name %s --api-key %s --json", integrationName, apiKey), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, _ *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.EncodeJSON(&fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "create"}, scenarios) +} + +func TestUpdateCommand(t *testing.T) { + const ( + integrationID = "integration-id-123" + apiKey = "a1b2c3d4" + ) + + scenarios := []testutil.CLIScenario{ + { + Args: fmt.Sprintf("--api-key %s", apiKey), + WantError: "error parsing arguments: required flag --id not provided", + }, + { + Args: fmt.Sprintf("--id %s", integrationID), + WantError: "error parsing arguments: required flag --api-key not provided", + }, + { + Args: fmt.Sprintf("--id %s --api-key %s", integrationID, apiKey), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { + return errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: fmt.Sprintf("--id %s --api-key %s", integrationID, apiKey), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, i *fastly.UpdateIntegrationInput) error { + if i.ID != integrationID { + return fmt.Errorf("unexpected id: %s", i.ID) + } + if fastly.ToValue(i.Type) != fastly.IntegrationTypeDatadog { + return fmt.Errorf("unexpected type: %s", fastly.ToValue(i.Type)) + } + if i.Config["apikey"] != apiKey { + return fmt.Errorf("unexpected apikey: %s", i.Config["apikey"]) + } + return nil + }, + }, + WantOutput: fstfmt.Success("Updated Datadog integration (id: %s)", integrationID), + }, + { + Args: fmt.Sprintf("--id %s --api-key %s --json", integrationID, apiKey), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { + return nil + }, + }, + WantOutput: fstfmt.JSON(`{"id": %q, "updated": true}`, integrationID), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "update"}, scenarios) +} diff --git a/pkg/commands/integration/datadog/doc.go b/pkg/commands/integration/datadog/doc.go new file mode 100644 index 000000000..790cfd1fd --- /dev/null +++ b/pkg/commands/integration/datadog/doc.go @@ -0,0 +1,3 @@ +// Package datadog contains commands to manage Datadog notification +// integrations. +package datadog diff --git a/pkg/commands/integration/datadog/root.go b/pkg/commands/integration/datadog/root.go new file mode 100644 index 000000000..0517d38ea --- /dev/null +++ b/pkg/commands/integration/datadog/root.go @@ -0,0 +1,31 @@ +package datadog + +import ( + "io" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/global" +) + +// CommandName is the string to be used to invoke this command. +const CommandName = "datadog" + +// 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 +} + +// 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, "Manage Datadog notification integrations") + return &c +} + +// Exec implements the command interface. +func (c *RootCommand) Exec(_ io.Reader, _ io.Writer) error { + panic("unreachable") +} diff --git a/pkg/commands/integration/datadog/update.go b/pkg/commands/integration/datadog/update.go new file mode 100644 index 000000000..84ab1e956 --- /dev/null +++ b/pkg/commands/integration/datadog/update.go @@ -0,0 +1,94 @@ +package datadog + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// UpdateCommand calls the Fastly API to update a Datadog notification integration. +type UpdateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + ID string + APIKey string + + // Optional. + IntegrationName argparser.OptionalString + Description argparser.OptionalString + Site argparser.OptionalString +} + +// NewUpdateCommand returns a usable command registered under the parent. +func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateCommand { + c := UpdateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("update", "Update a Datadog notification integration") + + // Required. + c.RegisterFlag(argparser.IntegrationIDFlag(&c.ID)) + c.CmdClause.Flag("api-key", "Datadog API key").Required().StringVar(&c.APIKey) + + // Optional. + c.CmdClause.Flag("name", "The name of the integration").Short('n').Action(c.IntegrationName.Set).StringVar(&c.IntegrationName.Value) + c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) + c.CmdClause.Flag("site", "Datadog site, e.g. \"datadoghq.eu\" (defaults to the US site)").Action(c.Site.Set).StringVar(&c.Site.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *UpdateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + config := fastly.DatadogConfig{APIKey: c.APIKey} + if c.Site.WasSet { + config.Site = c.Site.Value + } + + input := &fastly.UpdateIntegrationInput{ + ID: c.ID, + Type: fastly.ToPointer(fastly.IntegrationTypeDatadog), + Config: config.ToMap(), + } + if c.IntegrationName.WasSet { + input.Name = &c.IntegrationName.Value + } + if c.Description.WasSet { + input.Description = &c.Description.Value + } + + if err := c.Globals.APIClient.UpdateIntegration(context.TODO(), input); err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if c.JSONOutput.Enabled { + o := struct { + ID string `json:"id"` + Updated bool `json:"updated"` + }{ + c.ID, + true, + } + _, err := c.WriteJSON(out, o) + return err + } + + text.Success(out, "Updated Datadog integration (id: %s)", c.ID) + return nil +} diff --git a/pkg/commands/integration/delete.go b/pkg/commands/integration/delete.go new file mode 100644 index 000000000..8d76fbb54 --- /dev/null +++ b/pkg/commands/integration/delete.go @@ -0,0 +1,67 @@ +package integration + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// DeleteCommand calls the Fastly API to delete a notification integration. +type DeleteCommand struct { + argparser.Base + argparser.JSONOutput + + Input fastly.DeleteIntegrationInput +} + +// NewDeleteCommand returns a usable command registered under the parent. +func NewDeleteCommand(parent argparser.Registerer, g *global.Data) *DeleteCommand { + c := DeleteCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("delete", "Delete a notification integration") + + // Required. + c.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.Input.ID) + + // Optional. + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *DeleteCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + err := c.Globals.APIClient.DeleteIntegration(context.TODO(), &c.Input) + if err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if c.JSONOutput.Enabled { + o := struct { + ID string `json:"id"` + Deleted bool `json:"deleted"` + }{ + c.Input.ID, + true, + } + _, err := c.WriteJSON(out, o) + return err + } + + text.Success(out, "Deleted integration '%s'", c.Input.ID) + return nil +} diff --git a/pkg/commands/integration/describe.go b/pkg/commands/integration/describe.go new file mode 100644 index 000000000..6a4dfd673 --- /dev/null +++ b/pkg/commands/integration/describe.go @@ -0,0 +1,60 @@ +package integration + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// DescribeCommand calls the Fastly API to describe a notification integration. +type DescribeCommand struct { + argparser.Base + argparser.JSONOutput + + Input fastly.GetIntegrationInput +} + +// NewDescribeCommand returns a usable command registered under the parent. +func NewDescribeCommand(parent argparser.Registerer, g *global.Data) *DescribeCommand { + c := DescribeCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("describe", "Retrieve a single notification integration").Alias("get") + + // Required. + c.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.Input.ID) + + // Optional. + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *DescribeCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + o, err := c.Globals.APIClient.GetIntegration(context.TODO(), &c.Input) + if err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if ok, err := c.WriteJSON(out, o); ok { + return err + } + + text.PrintIntegration(out, o) + + return nil +} diff --git a/pkg/commands/integration/doc.go b/pkg/commands/integration/doc.go new file mode 100644 index 000000000..d8e118b30 --- /dev/null +++ b/pkg/commands/integration/doc.go @@ -0,0 +1,5 @@ +// Package integration contains commands to inspect and manipulate Fastly +// notification integrations. +// +// https://www.fastly.com/documentation/reference/api/observability/notifications/ +package integration diff --git a/pkg/commands/integration/integration_test.go b/pkg/commands/integration/integration_test.go new file mode 100644 index 000000000..f8b9f3055 --- /dev/null +++ b/pkg/commands/integration/integration_test.go @@ -0,0 +1,253 @@ +package integration_test + +import ( + "bytes" + "context" + "errors" + "fmt" + "testing" + "time" + + "github.com/fastly/go-fastly/v17/fastly" + + root "github.com/fastly/cli/pkg/commands/integration" + fstfmt "github.com/fastly/cli/pkg/fmt" + "github.com/fastly/cli/pkg/mock" + "github.com/fastly/cli/pkg/testutil" + "github.com/fastly/cli/pkg/text" +) + +func TestDescribeIntegrationCommand(t *testing.T) { + const ( + integrationName = "test123" + integrationID = "integration-id-123" + ) + + now := time.Now() + + scenarios := []testutil.CLIScenario{ + { + WantError: "error parsing arguments: required argument 'id' not provided", + }, + { + Args: integrationID, + API: &mock.API{ + GetIntegrationFn: func(_ context.Context, _ *fastly.GetIntegrationInput) (*fastly.Integration, error) { + return nil, errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: integrationID, + API: &mock.API{ + GetIntegrationFn: func(_ context.Context, i *fastly.GetIntegrationInput) (*fastly.Integration, error) { + return &fastly.Integration{ + ID: &i.ID, + Name: fastly.ToPointer(integrationName), + Type: fastly.ToPointer(fastly.IntegrationTypeDatadog), + Status: fastly.ToPointer("enabled"), + Config: map[string]string{"apikey": "abc123"}, + CreatedAt: &now, + }, nil + }, + }, + WantOutput: fmtIntegration(&fastly.Integration{ + ID: fastly.ToPointer(integrationID), + Name: fastly.ToPointer(integrationName), + Type: fastly.ToPointer(fastly.IntegrationTypeDatadog), + Status: fastly.ToPointer("enabled"), + Config: map[string]string{"apikey": "abc123"}, + CreatedAt: &now, + }), + }, + { + Args: fmt.Sprintf("%s --json", integrationID), + API: &mock.API{ + GetIntegrationFn: func(_ context.Context, i *fastly.GetIntegrationInput) (*fastly.Integration, error) { + return &fastly.Integration{ + ID: &i.ID, + Name: fastly.ToPointer(integrationName), + Type: fastly.ToPointer(fastly.IntegrationTypeDatadog), + CreatedAt: &now, + }, nil + }, + }, + WantOutput: fstfmt.EncodeJSON(&fastly.Integration{ + ID: fastly.ToPointer(integrationID), + Name: fastly.ToPointer(integrationName), + Type: fastly.ToPointer(fastly.IntegrationTypeDatadog), + CreatedAt: &now, + }), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, "describe"}, scenarios) +} + +func TestDeleteIntegrationCommand(t *testing.T) { + const integrationID = "integration-id-123" + errNotFound := errors.New("integration not found") + + scenarios := []testutil.CLIScenario{ + { + WantError: "error parsing arguments: required argument 'id' not provided", + }, + { + Args: "DOES-NOT-EXIST", + API: &mock.API{ + DeleteIntegrationFn: func(_ context.Context, i *fastly.DeleteIntegrationInput) error { + if i.ID != integrationID { + return errNotFound + } + return nil + }, + }, + WantError: errNotFound.Error(), + }, + { + Args: integrationID, + API: &mock.API{ + DeleteIntegrationFn: func(_ context.Context, i *fastly.DeleteIntegrationInput) error { + if i.ID != integrationID { + return errNotFound + } + return nil + }, + }, + WantOutput: fstfmt.Success("Deleted integration '%s'\n", integrationID), + }, + { + Args: fmt.Sprintf("%s --json", integrationID), + API: &mock.API{ + DeleteIntegrationFn: func(_ context.Context, i *fastly.DeleteIntegrationInput) error { + if i.ID != integrationID { + return errNotFound + } + return nil + }, + }, + WantOutput: fstfmt.JSON(`{"id": %q, "deleted": true}`, integrationID), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, "delete"}, scenarios) +} + +func TestListIntegrationsCommand(t *testing.T) { + const ( + integrationName = "test123" + integrationID = "integration-id-123" + ) + + now := time.Now() + + integrations := &fastly.SearchIntegrationsResponse{ + Data: []fastly.Integration{ + {ID: fastly.ToPointer(integrationID), Name: fastly.ToPointer(integrationName), Type: fastly.ToPointer(fastly.IntegrationTypeDatadog), CreatedAt: &now}, + {ID: fastly.ToPointer(integrationID + "+1"), Name: fastly.ToPointer(integrationName + "+1"), Type: fastly.ToPointer(fastly.IntegrationTypeOpsGenie), CreatedAt: &now}, + }, + } + + scenarios := []testutil.CLIScenario{ + { + API: &mock.API{ + SearchIntegrationsFn: func(_ context.Context, _ *fastly.SearchIntegrationsInput) (*fastly.SearchIntegrationsResponse, error) { + return nil, errors.New("unknown error") + }, + }, + WantError: "unknown error", + }, + { + API: &mock.API{ + SearchIntegrationsFn: func(_ context.Context, _ *fastly.SearchIntegrationsInput) (*fastly.SearchIntegrationsResponse, error) { + return integrations, nil + }, + }, + WantOutput: fmtIntegrations(integrations.Data), + }, + { + Args: fmt.Sprintf("--type %s", fastly.IntegrationTypeDatadog), + API: &mock.API{ + SearchIntegrationsFn: func(_ context.Context, i *fastly.SearchIntegrationsInput) (*fastly.SearchIntegrationsResponse, error) { + if fastly.ToValue(i.Type) != fastly.IntegrationTypeDatadog { + return nil, errors.New("unexpected type filter") + } + return integrations, nil + }, + }, + WantOutput: fmtIntegrations(integrations.Data), + }, + { + Args: "--json", + API: &mock.API{ + SearchIntegrationsFn: func(_ context.Context, _ *fastly.SearchIntegrationsInput) (*fastly.SearchIntegrationsResponse, error) { + return integrations, nil + }, + }, + WantOutput: fstfmt.EncodeJSON(integrations), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, "list"}, scenarios) +} + +func TestListTypesCommand(t *testing.T) { + types := []fastly.IntegrationType{ + { + Type: fastly.ToPointer(fastly.IntegrationTypeDatadog), + DisplayName: fastly.ToPointer("Datadog"), + CustomFields: []fastly.CustomField{ + {Name: fastly.ToPointer("apikey"), DisplayName: fastly.ToPointer("API Key"), Format: fastly.ToPointer("string")}, + }, + }, + } + + scenarios := []testutil.CLIScenario{ + { + API: &mock.API{ + GetIntegrationTypesFn: func(_ context.Context) (*[]fastly.IntegrationType, error) { + return nil, errors.New("unknown error") + }, + }, + WantError: "unknown error", + }, + { + API: &mock.API{ + GetIntegrationTypesFn: func(_ context.Context) (*[]fastly.IntegrationType, error) { + return &types, nil + }, + }, + WantOutput: fmtIntegrationTypes(types), + }, + { + Args: "--json", + API: &mock.API{ + GetIntegrationTypesFn: func(_ context.Context) (*[]fastly.IntegrationType, error) { + return &types, nil + }, + }, + WantOutput: fstfmt.EncodeJSON(&types), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, "list-types"}, scenarios) +} + +func fmtIntegration(i *fastly.Integration) string { + var b bytes.Buffer + text.PrintIntegration(&b, i) + return b.String() +} + +func fmtIntegrations(integrations []fastly.Integration) string { + var b bytes.Buffer + text.PrintIntegrationsTbl(&b, integrations) + return b.String() +} + +func fmtIntegrationTypes(types []fastly.IntegrationType) string { + var b bytes.Buffer + text.PrintIntegrationTypesTbl(&b, types) + return b.String() +} diff --git a/pkg/commands/integration/jiraissue/create.go b/pkg/commands/integration/jiraissue/create.go new file mode 100644 index 000000000..36a70e33a --- /dev/null +++ b/pkg/commands/integration/jiraissue/create.go @@ -0,0 +1,91 @@ +package jiraissue + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// CreateCommand calls the Fastly API to create a Jira Issue notification integration. +type CreateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + IntegrationName string + BaseURL string + Username string + APIToken string + ProjectKey string + IssueType string + + // Optional. + Description argparser.OptionalString +} + +// NewCreateCommand returns a usable command registered under the parent. +func NewCreateCommand(parent argparser.Registerer, g *global.Data) *CreateCommand { + c := CreateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("create", "Create a Jira Issue notification integration").Alias("add") + + // Required. + c.CmdClause.Flag("name", "The name of the integration").Short('n').Required().StringVar(&c.IntegrationName) + c.CmdClause.Flag("base-url", "The base URL of the Jira instance").Required().StringVar(&c.BaseURL) + c.CmdClause.Flag("username", "The Jira username (email address) used to authenticate").Required().StringVar(&c.Username) + c.CmdClause.Flag("api-token", "The Jira API token").Required().StringVar(&c.APIToken) + c.CmdClause.Flag("project-key", "The key of the Jira project where issues will be created").Required().StringVar(&c.ProjectKey) + c.CmdClause.Flag("issue-type", "The type of Jira issue to create").Required().StringVar(&c.IssueType) + + // Optional. + c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *CreateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + config := fastly.JiraIssueConfig{ + BaseURL: c.BaseURL, + Username: c.Username, + Token: c.APIToken, + ProjectKey: c.ProjectKey, + IssueType: c.IssueType, + } + + input := &fastly.CreateIntegrationInput{ + Name: &c.IntegrationName, + Type: fastly.ToPointer(fastly.IntegrationTypeJiraIssue), + Config: config.ToMap(), + } + if c.Description.WasSet { + input.Description = &c.Description.Value + } + + o, err := c.Globals.APIClient.CreateIntegration(context.TODO(), input) + if err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if ok, err := c.WriteJSON(out, o); ok { + return err + } + + text.Success(out, "Created Jira Issue integration '%s' (id: %s)", c.IntegrationName, fastly.ToValue(o.ID)) + return nil +} diff --git a/pkg/commands/integration/jiraissue/doc.go b/pkg/commands/integration/jiraissue/doc.go new file mode 100644 index 000000000..31d69a9e1 --- /dev/null +++ b/pkg/commands/integration/jiraissue/doc.go @@ -0,0 +1,3 @@ +// Package jiraissue contains commands to manage Jira Issue notification +// integrations. +package jiraissue diff --git a/pkg/commands/integration/jiraissue/jiraissue_test.go b/pkg/commands/integration/jiraissue/jiraissue_test.go new file mode 100644 index 000000000..b28fe9078 --- /dev/null +++ b/pkg/commands/integration/jiraissue/jiraissue_test.go @@ -0,0 +1,114 @@ +package jiraissue_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/fastly/go-fastly/v17/fastly" + + root "github.com/fastly/cli/pkg/commands/integration" + sub "github.com/fastly/cli/pkg/commands/integration/jiraissue" + fstfmt "github.com/fastly/cli/pkg/fmt" + "github.com/fastly/cli/pkg/mock" + "github.com/fastly/cli/pkg/testutil" +) + +const requiredFlags = "--base-url https://example.atlassian.net --username user@example.com --api-token abc123 --project-key PROJ --issue-type Bug" + +func TestCreateCommand(t *testing.T) { + const ( + integrationName = "test123" + integrationID = "integration-id-123" + ) + + scenarios := []testutil.CLIScenario{ + { + Args: requiredFlags, + WantError: "error parsing arguments: required flag --name not provided", + }, + { + Args: fmt.Sprintf("--name %s %s", integrationName, requiredFlags), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, _ *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return nil, errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: fmt.Sprintf("--name %s %s", integrationName, requiredFlags), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, i *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + if fastly.ToValue(i.Type) != fastly.IntegrationTypeJiraIssue { + return nil, fmt.Errorf("unexpected type: %s", fastly.ToValue(i.Type)) + } + if i.Config["baseurl"] != "https://example.atlassian.net" || i.Config["username"] != "user@example.com" || + i.Config["token"] != "abc123" || i.Config["projectkey"] != "PROJ" || i.Config["issuetype"] != "Bug" { + return nil, fmt.Errorf("unexpected config: %+v", i.Config) + } + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.Success("Created Jira Issue integration '%s' (id: %s)", integrationName, integrationID), + }, + { + Args: fmt.Sprintf("--name %s %s --json", integrationName, requiredFlags), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, _ *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.EncodeJSON(&fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "create"}, scenarios) +} + +func TestUpdateCommand(t *testing.T) { + const integrationID = "integration-id-123" + + scenarios := []testutil.CLIScenario{ + { + Args: requiredFlags, + WantError: "error parsing arguments: required flag --id not provided", + }, + { + Args: fmt.Sprintf("--id %s %s", integrationID, requiredFlags), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { + return errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: fmt.Sprintf("--id %s %s", integrationID, requiredFlags), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, i *fastly.UpdateIntegrationInput) error { + if i.ID != integrationID { + return fmt.Errorf("unexpected id: %s", i.ID) + } + if fastly.ToValue(i.Type) != fastly.IntegrationTypeJiraIssue { + return fmt.Errorf("unexpected type: %s", fastly.ToValue(i.Type)) + } + return nil + }, + }, + WantOutput: fstfmt.Success("Updated Jira Issue integration (id: %s)", integrationID), + }, + { + Args: fmt.Sprintf("--id %s %s --json", integrationID, requiredFlags), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { + return nil + }, + }, + WantOutput: fstfmt.JSON(`{"id": %q, "updated": true}`, integrationID), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "update"}, scenarios) +} diff --git a/pkg/commands/integration/jiraissue/root.go b/pkg/commands/integration/jiraissue/root.go new file mode 100644 index 000000000..780deae06 --- /dev/null +++ b/pkg/commands/integration/jiraissue/root.go @@ -0,0 +1,31 @@ +package jiraissue + +import ( + "io" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/global" +) + +// CommandName is the string to be used to invoke this command. +const CommandName = "jiraissue" + +// 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 +} + +// 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, "Manage Jira Issue notification integrations") + return &c +} + +// Exec implements the command interface. +func (c *RootCommand) Exec(_ io.Reader, _ io.Writer) error { + panic("unreachable") +} diff --git a/pkg/commands/integration/jiraissue/update.go b/pkg/commands/integration/jiraissue/update.go new file mode 100644 index 000000000..0372c6ba7 --- /dev/null +++ b/pkg/commands/integration/jiraissue/update.go @@ -0,0 +1,103 @@ +package jiraissue + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// UpdateCommand calls the Fastly API to update a Jira Issue notification integration. +type UpdateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + ID string + BaseURL string + Username string + APIToken string + ProjectKey string + IssueType string + + // Optional. + IntegrationName argparser.OptionalString + Description argparser.OptionalString +} + +// NewUpdateCommand returns a usable command registered under the parent. +func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateCommand { + c := UpdateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("update", "Update a Jira Issue notification integration") + + // Required. + c.RegisterFlag(argparser.IntegrationIDFlag(&c.ID)) + c.CmdClause.Flag("base-url", "The base URL of the Jira instance").Required().StringVar(&c.BaseURL) + c.CmdClause.Flag("username", "The Jira username (email address) used to authenticate").Required().StringVar(&c.Username) + c.CmdClause.Flag("api-token", "The Jira API token").Required().StringVar(&c.APIToken) + c.CmdClause.Flag("project-key", "The key of the Jira project where issues will be created").Required().StringVar(&c.ProjectKey) + c.CmdClause.Flag("issue-type", "The type of Jira issue to create").Required().StringVar(&c.IssueType) + + // Optional. + c.CmdClause.Flag("name", "The name of the integration").Short('n').Action(c.IntegrationName.Set).StringVar(&c.IntegrationName.Value) + c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *UpdateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + config := fastly.JiraIssueConfig{ + BaseURL: c.BaseURL, + Username: c.Username, + Token: c.APIToken, + ProjectKey: c.ProjectKey, + IssueType: c.IssueType, + } + + input := &fastly.UpdateIntegrationInput{ + ID: c.ID, + Type: fastly.ToPointer(fastly.IntegrationTypeJiraIssue), + Config: config.ToMap(), + } + if c.IntegrationName.WasSet { + input.Name = &c.IntegrationName.Value + } + if c.Description.WasSet { + input.Description = &c.Description.Value + } + + if err := c.Globals.APIClient.UpdateIntegration(context.TODO(), input); err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if c.JSONOutput.Enabled { + o := struct { + ID string `json:"id"` + Updated bool `json:"updated"` + }{ + c.ID, + true, + } + _, err := c.WriteJSON(out, o) + return err + } + + text.Success(out, "Updated Jira Issue integration (id: %s)", c.ID) + return nil +} diff --git a/pkg/commands/integration/jsm/create.go b/pkg/commands/integration/jsm/create.go new file mode 100644 index 000000000..8687731ee --- /dev/null +++ b/pkg/commands/integration/jsm/create.go @@ -0,0 +1,77 @@ +package jsm + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// CreateCommand calls the Fastly API to create a Jira Service Management notification integration. +type CreateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + IntegrationName string + APIKey string + + // Optional. + Description argparser.OptionalString +} + +// NewCreateCommand returns a usable command registered under the parent. +func NewCreateCommand(parent argparser.Registerer, g *global.Data) *CreateCommand { + c := CreateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("create", "Create a Jira Service Management notification integration").Alias("add") + + // Required. + c.CmdClause.Flag("name", "The name of the integration").Short('n').Required().StringVar(&c.IntegrationName) + c.CmdClause.Flag("api-key", "Jira Service Management API key").Required().StringVar(&c.APIKey) + + // Optional. + c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *CreateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + config := fastly.JSMConfig{APIKey: c.APIKey} + + input := &fastly.CreateIntegrationInput{ + Name: &c.IntegrationName, + Type: fastly.ToPointer(fastly.IntegrationTypeJSM), + Config: config.ToMap(), + } + if c.Description.WasSet { + input.Description = &c.Description.Value + } + + o, err := c.Globals.APIClient.CreateIntegration(context.TODO(), input) + if err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if ok, err := c.WriteJSON(out, o); ok { + return err + } + + text.Success(out, "Created Jira Service Management integration '%s' (id: %s)", c.IntegrationName, fastly.ToValue(o.ID)) + return nil +} diff --git a/pkg/commands/integration/jsm/doc.go b/pkg/commands/integration/jsm/doc.go new file mode 100644 index 000000000..72c6bb9a8 --- /dev/null +++ b/pkg/commands/integration/jsm/doc.go @@ -0,0 +1,3 @@ +// Package jsm contains commands to manage Jira Service Management +// notification integrations. +package jsm diff --git a/pkg/commands/integration/jsm/jsm_test.go b/pkg/commands/integration/jsm/jsm_test.go new file mode 100644 index 000000000..42a35c6f7 --- /dev/null +++ b/pkg/commands/integration/jsm/jsm_test.go @@ -0,0 +1,126 @@ +package jsm_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/fastly/go-fastly/v17/fastly" + + root "github.com/fastly/cli/pkg/commands/integration" + sub "github.com/fastly/cli/pkg/commands/integration/jsm" + fstfmt "github.com/fastly/cli/pkg/fmt" + "github.com/fastly/cli/pkg/mock" + "github.com/fastly/cli/pkg/testutil" +) + +func TestCreateCommand(t *testing.T) { + const ( + integrationName = "test123" + integrationID = "integration-id-123" + apiKey = "a1b2c3d4" + ) + + scenarios := []testutil.CLIScenario{ + { + Args: fmt.Sprintf("--api-key %s", apiKey), + WantError: "error parsing arguments: required flag --name not provided", + }, + { + Args: fmt.Sprintf("--name %s", integrationName), + WantError: "error parsing arguments: required flag --api-key not provided", + }, + { + Args: fmt.Sprintf("--name %s --api-key %s", integrationName, apiKey), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, _ *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return nil, errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: fmt.Sprintf("--name %s --api-key %s", integrationName, apiKey), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, i *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + if fastly.ToValue(i.Type) != fastly.IntegrationTypeJSM { + return nil, fmt.Errorf("unexpected type: %s", fastly.ToValue(i.Type)) + } + if i.Config["apikey"] != apiKey { + return nil, fmt.Errorf("unexpected apikey: %s", i.Config["apikey"]) + } + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.Success("Created Jira Service Management integration '%s' (id: %s)", integrationName, integrationID), + }, + { + Args: fmt.Sprintf("--name %s --api-key %s --json", integrationName, apiKey), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, _ *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.EncodeJSON(&fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "create"}, scenarios) +} + +func TestUpdateCommand(t *testing.T) { + const ( + integrationID = "integration-id-123" + apiKey = "a1b2c3d4" + ) + + scenarios := []testutil.CLIScenario{ + { + Args: fmt.Sprintf("--api-key %s", apiKey), + WantError: "error parsing arguments: required flag --id not provided", + }, + { + Args: fmt.Sprintf("--id %s", integrationID), + WantError: "error parsing arguments: required flag --api-key not provided", + }, + { + Args: fmt.Sprintf("--id %s --api-key %s", integrationID, apiKey), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { + return errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: fmt.Sprintf("--id %s --api-key %s", integrationID, apiKey), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, i *fastly.UpdateIntegrationInput) error { + if i.ID != integrationID { + return fmt.Errorf("unexpected id: %s", i.ID) + } + if fastly.ToValue(i.Type) != fastly.IntegrationTypeJSM { + return fmt.Errorf("unexpected type: %s", fastly.ToValue(i.Type)) + } + if i.Config["apikey"] != apiKey { + return fmt.Errorf("unexpected apikey: %s", i.Config["apikey"]) + } + return nil + }, + }, + WantOutput: fstfmt.Success("Updated Jira Service Management integration (id: %s)", integrationID), + }, + { + Args: fmt.Sprintf("--id %s --api-key %s --json", integrationID, apiKey), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { + return nil + }, + }, + WantOutput: fstfmt.JSON(`{"id": %q, "updated": true}`, integrationID), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "update"}, scenarios) +} diff --git a/pkg/commands/integration/jsm/root.go b/pkg/commands/integration/jsm/root.go new file mode 100644 index 000000000..9ba2a86f7 --- /dev/null +++ b/pkg/commands/integration/jsm/root.go @@ -0,0 +1,31 @@ +package jsm + +import ( + "io" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/global" +) + +// CommandName is the string to be used to invoke this command. +const CommandName = "jsm" + +// 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 +} + +// 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, "Manage Jira Service Management notification integrations") + return &c +} + +// Exec implements the command interface. +func (c *RootCommand) Exec(_ io.Reader, _ io.Writer) error { + panic("unreachable") +} diff --git a/pkg/commands/integration/jsm/update.go b/pkg/commands/integration/jsm/update.go new file mode 100644 index 000000000..a9e08ab80 --- /dev/null +++ b/pkg/commands/integration/jsm/update.go @@ -0,0 +1,89 @@ +package jsm + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// UpdateCommand calls the Fastly API to update a Jira Service Management notification integration. +type UpdateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + ID string + APIKey string + + // Optional. + IntegrationName argparser.OptionalString + Description argparser.OptionalString +} + +// NewUpdateCommand returns a usable command registered under the parent. +func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateCommand { + c := UpdateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("update", "Update a Jira Service Management notification integration") + + // Required. + c.RegisterFlag(argparser.IntegrationIDFlag(&c.ID)) + c.CmdClause.Flag("api-key", "Jira Service Management API key").Required().StringVar(&c.APIKey) + + // Optional. + c.CmdClause.Flag("name", "The name of the integration").Short('n').Action(c.IntegrationName.Set).StringVar(&c.IntegrationName.Value) + c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *UpdateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + config := fastly.JSMConfig{APIKey: c.APIKey} + + input := &fastly.UpdateIntegrationInput{ + ID: c.ID, + Type: fastly.ToPointer(fastly.IntegrationTypeJSM), + Config: config.ToMap(), + } + if c.IntegrationName.WasSet { + input.Name = &c.IntegrationName.Value + } + if c.Description.WasSet { + input.Description = &c.Description.Value + } + + if err := c.Globals.APIClient.UpdateIntegration(context.TODO(), input); err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if c.JSONOutput.Enabled { + o := struct { + ID string `json:"id"` + Updated bool `json:"updated"` + }{ + c.ID, + true, + } + _, err := c.WriteJSON(out, o) + return err + } + + text.Success(out, "Updated Jira Service Management integration (id: %s)", c.ID) + return nil +} diff --git a/pkg/commands/integration/list.go b/pkg/commands/integration/list.go new file mode 100644 index 000000000..b4861a26c --- /dev/null +++ b/pkg/commands/integration/list.go @@ -0,0 +1,89 @@ +package integration + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// knownIntegrationTypes lists the integration type values with dedicated CLI +// sub-families, offered as shell-completion hints for --type. Other type +// values (e.g. legacy integration types) are still accepted. +var knownIntegrationTypes = []string{ + fastly.IntegrationTypeDatadog, + fastly.IntegrationTypeJiraIssue, + fastly.IntegrationTypeJSM, + fastly.IntegrationTypeOpsGenie, + fastly.IntegrationTypeSplunkOnCall, +} + +// ListCommand calls the Fastly API to list notification integrations. +type ListCommand struct { + argparser.Base + argparser.JSONOutput + + Cursor argparser.OptionalString + Limit argparser.OptionalInt + Sort argparser.OptionalString + Type argparser.OptionalString +} + +// NewListCommand returns a usable command registered under the parent. +func NewListCommand(parent argparser.Registerer, g *global.Data) *ListCommand { + c := ListCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("list", "List notification integrations") + + // Optional. + c.CmdClause.Flag("cursor", "Pagination cursor from a previous response's meta").Action(c.Cursor.Set).StringVar(&c.Cursor.Value) + c.RegisterFlagBool(c.JSONFlag()) + c.CmdClause.Flag("limit", "Maximum number of items to return").Action(c.Limit.Set).IntVar(&c.Limit.Value) + c.CmdClause.Flag("sort", "Field to sort results by").Action(c.Sort.Set).StringVar(&c.Sort.Value) + c.CmdClause.Flag("type", "Filter integrations by type").HintOptions(knownIntegrationTypes...).Action(c.Type.Set).StringVar(&c.Type.Value) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *ListCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + input := &fastly.SearchIntegrationsInput{} + if c.Cursor.WasSet { + input.Cursor = &c.Cursor.Value + } + if c.Limit.WasSet { + input.Limit = &c.Limit.Value + } + if c.Sort.WasSet { + input.Sort = &c.Sort.Value + } + if c.Type.WasSet { + input.Type = &c.Type.Value + } + + o, err := c.Globals.APIClient.SearchIntegrations(context.TODO(), input) + if err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if ok, err := c.WriteJSON(out, o); ok { + return err + } + + text.PrintIntegrationsTbl(out, o.Data) + + return nil +} diff --git a/pkg/commands/integration/list_types.go b/pkg/commands/integration/list_types.go new file mode 100644 index 000000000..e27d13f27 --- /dev/null +++ b/pkg/commands/integration/list_types.go @@ -0,0 +1,60 @@ +package integration + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// ListTypesCommand calls the Fastly API to list the supported notification +// integration types and the configuration each one requires. +type ListTypesCommand struct { + argparser.Base + argparser.JSONOutput +} + +// NewListTypesCommand returns a usable command registered under the parent. +func NewListTypesCommand(parent argparser.Registerer, g *global.Data) *ListTypesCommand { + c := ListTypesCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("list-types", "List supported notification integration types") + + // Optional. + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *ListTypesCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + o, err := c.Globals.APIClient.GetIntegrationTypes(context.TODO()) + if err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if ok, err := c.WriteJSON(out, o); ok { + return err + } + + var types []fastly.IntegrationType + if o != nil { + types = *o + } + text.PrintIntegrationTypesTbl(out, types) + + return nil +} diff --git a/pkg/commands/integration/opsgenie/create.go b/pkg/commands/integration/opsgenie/create.go new file mode 100644 index 000000000..4cbe1b259 --- /dev/null +++ b/pkg/commands/integration/opsgenie/create.go @@ -0,0 +1,77 @@ +package opsgenie + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// CreateCommand calls the Fastly API to create an OpsGenie notification integration. +type CreateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + IntegrationName string + APIKey string + + // Optional. + Description argparser.OptionalString +} + +// NewCreateCommand returns a usable command registered under the parent. +func NewCreateCommand(parent argparser.Registerer, g *global.Data) *CreateCommand { + c := CreateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("create", "Create an OpsGenie notification integration").Alias("add") + + // Required. + c.CmdClause.Flag("name", "The name of the integration").Short('n').Required().StringVar(&c.IntegrationName) + c.CmdClause.Flag("api-key", "OpsGenie API key").Required().StringVar(&c.APIKey) + + // Optional. + c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *CreateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + config := fastly.OpsGenieConfig{APIKey: c.APIKey} + + input := &fastly.CreateIntegrationInput{ + Name: &c.IntegrationName, + Type: fastly.ToPointer(fastly.IntegrationTypeOpsGenie), + Config: config.ToMap(), + } + if c.Description.WasSet { + input.Description = &c.Description.Value + } + + o, err := c.Globals.APIClient.CreateIntegration(context.TODO(), input) + if err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if ok, err := c.WriteJSON(out, o); ok { + return err + } + + text.Success(out, "Created OpsGenie integration '%s' (id: %s)", c.IntegrationName, fastly.ToValue(o.ID)) + return nil +} diff --git a/pkg/commands/integration/opsgenie/doc.go b/pkg/commands/integration/opsgenie/doc.go new file mode 100644 index 000000000..b18e05e4c --- /dev/null +++ b/pkg/commands/integration/opsgenie/doc.go @@ -0,0 +1,3 @@ +// Package opsgenie contains commands to manage OpsGenie notification +// integrations. +package opsgenie diff --git a/pkg/commands/integration/opsgenie/opsgenie_test.go b/pkg/commands/integration/opsgenie/opsgenie_test.go new file mode 100644 index 000000000..5484eeefd --- /dev/null +++ b/pkg/commands/integration/opsgenie/opsgenie_test.go @@ -0,0 +1,126 @@ +package opsgenie_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/fastly/go-fastly/v17/fastly" + + root "github.com/fastly/cli/pkg/commands/integration" + sub "github.com/fastly/cli/pkg/commands/integration/opsgenie" + fstfmt "github.com/fastly/cli/pkg/fmt" + "github.com/fastly/cli/pkg/mock" + "github.com/fastly/cli/pkg/testutil" +) + +func TestCreateCommand(t *testing.T) { + const ( + integrationName = "test123" + integrationID = "integration-id-123" + apiKey = "a1b2c3d4" + ) + + scenarios := []testutil.CLIScenario{ + { + Args: fmt.Sprintf("--api-key %s", apiKey), + WantError: "error parsing arguments: required flag --name not provided", + }, + { + Args: fmt.Sprintf("--name %s", integrationName), + WantError: "error parsing arguments: required flag --api-key not provided", + }, + { + Args: fmt.Sprintf("--name %s --api-key %s", integrationName, apiKey), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, _ *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return nil, errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: fmt.Sprintf("--name %s --api-key %s", integrationName, apiKey), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, i *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + if fastly.ToValue(i.Type) != fastly.IntegrationTypeOpsGenie { + return nil, fmt.Errorf("unexpected type: %s", fastly.ToValue(i.Type)) + } + if i.Config["apikey"] != apiKey { + return nil, fmt.Errorf("unexpected apikey: %s", i.Config["apikey"]) + } + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.Success("Created OpsGenie integration '%s' (id: %s)", integrationName, integrationID), + }, + { + Args: fmt.Sprintf("--name %s --api-key %s --json", integrationName, apiKey), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, _ *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.EncodeJSON(&fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "create"}, scenarios) +} + +func TestUpdateCommand(t *testing.T) { + const ( + integrationID = "integration-id-123" + apiKey = "a1b2c3d4" + ) + + scenarios := []testutil.CLIScenario{ + { + Args: fmt.Sprintf("--api-key %s", apiKey), + WantError: "error parsing arguments: required flag --id not provided", + }, + { + Args: fmt.Sprintf("--id %s", integrationID), + WantError: "error parsing arguments: required flag --api-key not provided", + }, + { + Args: fmt.Sprintf("--id %s --api-key %s", integrationID, apiKey), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { + return errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: fmt.Sprintf("--id %s --api-key %s", integrationID, apiKey), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, i *fastly.UpdateIntegrationInput) error { + if i.ID != integrationID { + return fmt.Errorf("unexpected id: %s", i.ID) + } + if fastly.ToValue(i.Type) != fastly.IntegrationTypeOpsGenie { + return fmt.Errorf("unexpected type: %s", fastly.ToValue(i.Type)) + } + if i.Config["apikey"] != apiKey { + return fmt.Errorf("unexpected apikey: %s", i.Config["apikey"]) + } + return nil + }, + }, + WantOutput: fstfmt.Success("Updated OpsGenie integration (id: %s)", integrationID), + }, + { + Args: fmt.Sprintf("--id %s --api-key %s --json", integrationID, apiKey), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { + return nil + }, + }, + WantOutput: fstfmt.JSON(`{"id": %q, "updated": true}`, integrationID), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "update"}, scenarios) +} diff --git a/pkg/commands/integration/opsgenie/root.go b/pkg/commands/integration/opsgenie/root.go new file mode 100644 index 000000000..64fdc4d83 --- /dev/null +++ b/pkg/commands/integration/opsgenie/root.go @@ -0,0 +1,31 @@ +package opsgenie + +import ( + "io" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/global" +) + +// CommandName is the string to be used to invoke this command. +const CommandName = "opsgenie" + +// 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 +} + +// 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, "Manage OpsGenie notification integrations") + return &c +} + +// Exec implements the command interface. +func (c *RootCommand) Exec(_ io.Reader, _ io.Writer) error { + panic("unreachable") +} diff --git a/pkg/commands/integration/opsgenie/update.go b/pkg/commands/integration/opsgenie/update.go new file mode 100644 index 000000000..d577fe00f --- /dev/null +++ b/pkg/commands/integration/opsgenie/update.go @@ -0,0 +1,89 @@ +package opsgenie + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// UpdateCommand calls the Fastly API to update an OpsGenie notification integration. +type UpdateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + ID string + APIKey string + + // Optional. + IntegrationName argparser.OptionalString + Description argparser.OptionalString +} + +// NewUpdateCommand returns a usable command registered under the parent. +func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateCommand { + c := UpdateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("update", "Update an OpsGenie notification integration") + + // Required. + c.RegisterFlag(argparser.IntegrationIDFlag(&c.ID)) + c.CmdClause.Flag("api-key", "OpsGenie API key").Required().StringVar(&c.APIKey) + + // Optional. + c.CmdClause.Flag("name", "The name of the integration").Short('n').Action(c.IntegrationName.Set).StringVar(&c.IntegrationName.Value) + c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *UpdateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + config := fastly.OpsGenieConfig{APIKey: c.APIKey} + + input := &fastly.UpdateIntegrationInput{ + ID: c.ID, + Type: fastly.ToPointer(fastly.IntegrationTypeOpsGenie), + Config: config.ToMap(), + } + if c.IntegrationName.WasSet { + input.Name = &c.IntegrationName.Value + } + if c.Description.WasSet { + input.Description = &c.Description.Value + } + + if err := c.Globals.APIClient.UpdateIntegration(context.TODO(), input); err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if c.JSONOutput.Enabled { + o := struct { + ID string `json:"id"` + Updated bool `json:"updated"` + }{ + c.ID, + true, + } + _, err := c.WriteJSON(out, o) + return err + } + + text.Success(out, "Updated OpsGenie integration (id: %s)", c.ID) + return nil +} diff --git a/pkg/commands/integration/root.go b/pkg/commands/integration/root.go new file mode 100644 index 000000000..c67a9802d --- /dev/null +++ b/pkg/commands/integration/root.go @@ -0,0 +1,31 @@ +package integration + +import ( + "io" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/global" +) + +// CommandName is the string to be used to invoke this command. +const CommandName = "integration" + +// 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 +} + +// 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, "Manage Fastly notification integrations") + return &c +} + +// Exec implements the command interface. +func (c *RootCommand) Exec(_ io.Reader, _ io.Writer) error { + panic("unreachable") +} diff --git a/pkg/commands/integration/splunkoncall/create.go b/pkg/commands/integration/splunkoncall/create.go new file mode 100644 index 000000000..c4b38c9b3 --- /dev/null +++ b/pkg/commands/integration/splunkoncall/create.go @@ -0,0 +1,77 @@ +package splunkoncall + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// CreateCommand calls the Fastly API to create a Splunk On-Call notification integration. +type CreateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + IntegrationName string + URL string + + // Optional. + Description argparser.OptionalString +} + +// NewCreateCommand returns a usable command registered under the parent. +func NewCreateCommand(parent argparser.Registerer, g *global.Data) *CreateCommand { + c := CreateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("create", "Create a Splunk On-Call notification integration").Alias("add") + + // Required. + c.CmdClause.Flag("name", "The name of the integration").Short('n').Required().StringVar(&c.IntegrationName) + c.CmdClause.Flag("url", "The Splunk On-Call webhook URL").Required().StringVar(&c.URL) + + // Optional. + c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *CreateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + config := fastly.SplunkOnCallConfig{URL: c.URL} + + input := &fastly.CreateIntegrationInput{ + Name: &c.IntegrationName, + Type: fastly.ToPointer(fastly.IntegrationTypeSplunkOnCall), + Config: config.ToMap(), + } + if c.Description.WasSet { + input.Description = &c.Description.Value + } + + o, err := c.Globals.APIClient.CreateIntegration(context.TODO(), input) + if err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if ok, err := c.WriteJSON(out, o); ok { + return err + } + + text.Success(out, "Created Splunk On-Call integration '%s' (id: %s)", c.IntegrationName, fastly.ToValue(o.ID)) + return nil +} diff --git a/pkg/commands/integration/splunkoncall/doc.go b/pkg/commands/integration/splunkoncall/doc.go new file mode 100644 index 000000000..8bcf81e55 --- /dev/null +++ b/pkg/commands/integration/splunkoncall/doc.go @@ -0,0 +1,3 @@ +// Package splunkoncall contains commands to manage Splunk On-Call +// notification integrations. +package splunkoncall diff --git a/pkg/commands/integration/splunkoncall/root.go b/pkg/commands/integration/splunkoncall/root.go new file mode 100644 index 000000000..85ade0baa --- /dev/null +++ b/pkg/commands/integration/splunkoncall/root.go @@ -0,0 +1,31 @@ +package splunkoncall + +import ( + "io" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/global" +) + +// CommandName is the string to be used to invoke this command. +const CommandName = "splunkoncall" + +// 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 +} + +// 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, "Manage Splunk On-Call notification integrations") + return &c +} + +// Exec implements the command interface. +func (c *RootCommand) Exec(_ io.Reader, _ io.Writer) error { + panic("unreachable") +} diff --git a/pkg/commands/integration/splunkoncall/splunkoncall_test.go b/pkg/commands/integration/splunkoncall/splunkoncall_test.go new file mode 100644 index 000000000..3241dedf7 --- /dev/null +++ b/pkg/commands/integration/splunkoncall/splunkoncall_test.go @@ -0,0 +1,126 @@ +package splunkoncall_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/fastly/go-fastly/v17/fastly" + + root "github.com/fastly/cli/pkg/commands/integration" + sub "github.com/fastly/cli/pkg/commands/integration/splunkoncall" + fstfmt "github.com/fastly/cli/pkg/fmt" + "github.com/fastly/cli/pkg/mock" + "github.com/fastly/cli/pkg/testutil" +) + +func TestCreateCommand(t *testing.T) { + const ( + integrationName = "test123" + integrationID = "integration-id-123" + webhookURL = "https://alert.victorops.com/integrations/generic/xyz" + ) + + scenarios := []testutil.CLIScenario{ + { + Args: fmt.Sprintf("--url %s", webhookURL), + WantError: "error parsing arguments: required flag --name not provided", + }, + { + Args: fmt.Sprintf("--name %s", integrationName), + WantError: "error parsing arguments: required flag --url not provided", + }, + { + Args: fmt.Sprintf("--name %s --url %s", integrationName, webhookURL), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, _ *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return nil, errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: fmt.Sprintf("--name %s --url %s", integrationName, webhookURL), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, i *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + if fastly.ToValue(i.Type) != fastly.IntegrationTypeSplunkOnCall { + return nil, fmt.Errorf("unexpected type: %s", fastly.ToValue(i.Type)) + } + if i.Config["url"] != webhookURL { + return nil, fmt.Errorf("unexpected url: %s", i.Config["url"]) + } + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.Success("Created Splunk On-Call integration '%s' (id: %s)", integrationName, integrationID), + }, + { + Args: fmt.Sprintf("--name %s --url %s --json", integrationName, webhookURL), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, _ *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.EncodeJSON(&fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "create"}, scenarios) +} + +func TestUpdateCommand(t *testing.T) { + const ( + integrationID = "integration-id-123" + webhookURL = "https://alert.victorops.com/integrations/generic/xyz" + ) + + scenarios := []testutil.CLIScenario{ + { + Args: fmt.Sprintf("--url %s", webhookURL), + WantError: "error parsing arguments: required flag --id not provided", + }, + { + Args: fmt.Sprintf("--id %s", integrationID), + WantError: "error parsing arguments: required flag --url not provided", + }, + { + Args: fmt.Sprintf("--id %s --url %s", integrationID, webhookURL), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { + return errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: fmt.Sprintf("--id %s --url %s", integrationID, webhookURL), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, i *fastly.UpdateIntegrationInput) error { + if i.ID != integrationID { + return fmt.Errorf("unexpected id: %s", i.ID) + } + if fastly.ToValue(i.Type) != fastly.IntegrationTypeSplunkOnCall { + return fmt.Errorf("unexpected type: %s", fastly.ToValue(i.Type)) + } + if i.Config["url"] != webhookURL { + return fmt.Errorf("unexpected url: %s", i.Config["url"]) + } + return nil + }, + }, + WantOutput: fstfmt.Success("Updated Splunk On-Call integration (id: %s)", integrationID), + }, + { + Args: fmt.Sprintf("--id %s --url %s --json", integrationID, webhookURL), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { + return nil + }, + }, + WantOutput: fstfmt.JSON(`{"id": %q, "updated": true}`, integrationID), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "update"}, scenarios) +} diff --git a/pkg/commands/integration/splunkoncall/update.go b/pkg/commands/integration/splunkoncall/update.go new file mode 100644 index 000000000..4b5602031 --- /dev/null +++ b/pkg/commands/integration/splunkoncall/update.go @@ -0,0 +1,89 @@ +package splunkoncall + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// UpdateCommand calls the Fastly API to update a Splunk On-Call notification integration. +type UpdateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + ID string + URL string + + // Optional. + IntegrationName argparser.OptionalString + Description argparser.OptionalString +} + +// NewUpdateCommand returns a usable command registered under the parent. +func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateCommand { + c := UpdateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("update", "Update a Splunk On-Call notification integration") + + // Required. + c.RegisterFlag(argparser.IntegrationIDFlag(&c.ID)) + c.CmdClause.Flag("url", "The Splunk On-Call webhook URL").Required().StringVar(&c.URL) + + // Optional. + c.CmdClause.Flag("name", "The name of the integration").Short('n').Action(c.IntegrationName.Set).StringVar(&c.IntegrationName.Value) + c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *UpdateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + config := fastly.SplunkOnCallConfig{URL: c.URL} + + input := &fastly.UpdateIntegrationInput{ + ID: c.ID, + Type: fastly.ToPointer(fastly.IntegrationTypeSplunkOnCall), + Config: config.ToMap(), + } + if c.IntegrationName.WasSet { + input.Name = &c.IntegrationName.Value + } + if c.Description.WasSet { + input.Description = &c.Description.Value + } + + if err := c.Globals.APIClient.UpdateIntegration(context.TODO(), input); err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if c.JSONOutput.Enabled { + o := struct { + ID string `json:"id"` + Updated bool `json:"updated"` + }{ + c.ID, + true, + } + _, err := c.WriteJSON(out, o) + return err + } + + text.Success(out, "Updated Splunk On-Call integration (id: %s)", c.ID) + return nil +} diff --git a/pkg/mock/api.go b/pkg/mock/api.go index 468056473..8b85800c0 100644 --- a/pkg/mock/api.go +++ b/pkg/mock/api.go @@ -347,6 +347,13 @@ type API struct { UpdateServiceAuthorizationFn func(context.Context, *fastly.UpdateServiceAuthorizationInput) (*fastly.ServiceAuthorization, error) DeleteServiceAuthorizationFn func(context.Context, *fastly.DeleteServiceAuthorizationInput) error + SearchIntegrationsFn func(context.Context, *fastly.SearchIntegrationsInput) (*fastly.SearchIntegrationsResponse, error) + CreateIntegrationFn func(context.Context, *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) + GetIntegrationFn func(context.Context, *fastly.GetIntegrationInput) (*fastly.Integration, error) + UpdateIntegrationFn func(context.Context, *fastly.UpdateIntegrationInput) error + DeleteIntegrationFn func(context.Context, *fastly.DeleteIntegrationInput) error + GetIntegrationTypesFn func(context.Context) (*[]fastly.IntegrationType, error) + CreateConfigStoreFn func(context.Context, *fastly.CreateConfigStoreInput) (*fastly.ConfigStore, error) DeleteConfigStoreFn func(context.Context, *fastly.DeleteConfigStoreInput) error GetConfigStoreFn func(context.Context, *fastly.GetConfigStoreInput) (*fastly.ConfigStore, error) @@ -1819,6 +1826,36 @@ func (m API) DeleteServiceAuthorization(ctx context.Context, i *fastly.DeleteSer return m.DeleteServiceAuthorizationFn(ctx, i) } +// SearchIntegrations implements Interface. +func (m API) SearchIntegrations(ctx context.Context, i *fastly.SearchIntegrationsInput) (*fastly.SearchIntegrationsResponse, error) { + return m.SearchIntegrationsFn(ctx, i) +} + +// CreateIntegration implements Interface. +func (m API) CreateIntegration(ctx context.Context, i *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return m.CreateIntegrationFn(ctx, i) +} + +// GetIntegration implements Interface. +func (m API) GetIntegration(ctx context.Context, i *fastly.GetIntegrationInput) (*fastly.Integration, error) { + return m.GetIntegrationFn(ctx, i) +} + +// UpdateIntegration implements Interface. +func (m API) UpdateIntegration(ctx context.Context, i *fastly.UpdateIntegrationInput) error { + return m.UpdateIntegrationFn(ctx, i) +} + +// DeleteIntegration implements Interface. +func (m API) DeleteIntegration(ctx context.Context, i *fastly.DeleteIntegrationInput) error { + return m.DeleteIntegrationFn(ctx, i) +} + +// GetIntegrationTypes implements Interface. +func (m API) GetIntegrationTypes(ctx context.Context) (*[]fastly.IntegrationType, error) { + return m.GetIntegrationTypesFn(ctx) +} + // CreateConfigStore implements Interface. func (m API) CreateConfigStore(ctx context.Context, i *fastly.CreateConfigStoreInput) (*fastly.ConfigStore, error) { return m.CreateConfigStoreFn(ctx, i) diff --git a/pkg/text/integration.go b/pkg/text/integration.go new file mode 100644 index 000000000..9b83b8d80 --- /dev/null +++ b/pkg/text/integration.go @@ -0,0 +1,84 @@ +package text + +import ( + "fmt" + "io" + "sort" + "strings" + "time" + + "github.com/fastly/go-fastly/v17/fastly" + + fsttime "github.com/fastly/cli/pkg/time" +) + +func fmtIntegrationTime(t *time.Time) string { + if t == nil { + return "n/a" + } + return t.UTC().Format(fsttime.Format) +} + +// PrintIntegrationsTbl displays integrations in a table format. +func PrintIntegrationsTbl(out io.Writer, integrations []fastly.Integration) { + tbl := NewTable(out) + tbl.AddHeader("Name", "ID", "Type", "Status", "Created (UTC)", "Updated (UTC)") + for _, i := range integrations { + tbl.AddLine( + fastly.ToValue(i.Name), + fastly.ToValue(i.ID), + fastly.ToValue(i.Type), + fastly.ToValue(i.Status), + fmtIntegrationTime(i.CreatedAt), + fmtIntegrationTime(i.UpdatedAt), + ) + } + tbl.Print() +} + +// PrintIntegration displays detailed information about a single integration. +func PrintIntegration(out io.Writer, i *fastly.Integration) { + PrintLines(out, Lines{ + "Name": fastly.ToValue(i.Name), + "ID": fastly.ToValue(i.ID), + "Type": fastly.ToValue(i.Type), + "Status": fastly.ToValue(i.Status), + "Description": fastly.ToValue(i.Description), + "Created (UTC)": fmtIntegrationTime(i.CreatedAt), + "Updated (UTC)": fmtIntegrationTime(i.UpdatedAt), + }) + + if len(i.Config) == 0 { + return + } + + keys := make([]string, 0, len(i.Config)) + for k := range i.Config { + keys = append(keys, k) + } + sort.Strings(keys) + + fmt.Fprint(out, "Config:\n") + for _, k := range keys { + fmt.Fprintf(out, "\t%s: %s\n", k, i.Config[k]) + } +} + +// PrintIntegrationTypesTbl displays the supported integration types and their +// configuration fields in a table format. +func PrintIntegrationTypesTbl(out io.Writer, types []fastly.IntegrationType) { + tbl := NewTable(out) + tbl.AddHeader("Type", "Display Name", "Custom Fields") + for _, t := range types { + fields := make([]string, 0, len(t.CustomFields)) + for _, f := range t.CustomFields { + name := fastly.ToValue(f.Name) + if format := fastly.ToValue(f.Format); format != "" { + name = fmt.Sprintf("%s (%s)", name, format) + } + fields = append(fields, name) + } + tbl.AddLine(fastly.ToValue(t.Type), fastly.ToValue(t.DisplayName), strings.Join(fields, ", ")) + } + tbl.Print() +} From bee1c22b7abf481be00308775422f3fd060c27ac Mon Sep 17 00:00:00 2001 From: Anthony Gomez Date: Fri, 31 Jul 2026 11:27:57 -0400 Subject: [PATCH 2/5] added missing integration types and made ID positional for all integration commands --- pkg/api/interface.go | 3 + pkg/app/metadata.json | 87 +++++++ pkg/argparser/common.go | 10 - pkg/commands/commands.go | 48 ++++ .../integration/datadog/datadog_test.go | 10 +- pkg/commands/integration/datadog/update.go | 2 +- .../integration/jiraissue/jiraissue_test.go | 8 +- pkg/commands/integration/jiraissue/update.go | 2 +- pkg/commands/integration/jsm/jsm_test.go | 10 +- pkg/commands/integration/jsm/update.go | 2 +- pkg/commands/integration/list.go | 12 + pkg/commands/integration/mail/confirm.go | 68 ++++++ pkg/commands/integration/mail/create.go | 75 ++++++ pkg/commands/integration/mail/doc.go | 3 + pkg/commands/integration/mail/mail_test.go | 168 ++++++++++++++ pkg/commands/integration/mail/root.go | 31 +++ pkg/commands/integration/mail/update.go | 87 +++++++ pkg/commands/integration/msteams/create.go | 75 ++++++ pkg/commands/integration/msteams/doc.go | 3 + .../integration/msteams/msteams_test.go | 126 ++++++++++ pkg/commands/integration/msteams/root.go | 31 +++ pkg/commands/integration/msteams/update.go | 87 +++++++ pkg/commands/integration/newrelic/create.go | 77 +++++++ pkg/commands/integration/newrelic/doc.go | 3 + .../integration/newrelic/newrelic_test.go | 113 +++++++++ pkg/commands/integration/newrelic/root.go | 31 +++ pkg/commands/integration/newrelic/update.go | 89 ++++++++ .../integration/opsgenie/opsgenie_test.go | 10 +- pkg/commands/integration/opsgenie/update.go | 2 +- pkg/commands/integration/pagerduty/create.go | 75 ++++++ pkg/commands/integration/pagerduty/doc.go | 3 + .../integration/pagerduty/pagerduty_test.go | 126 ++++++++++ pkg/commands/integration/pagerduty/root.go | 31 +++ pkg/commands/integration/pagerduty/update.go | 87 +++++++ pkg/commands/integration/slack/create.go | 75 ++++++ pkg/commands/integration/slack/doc.go | 2 + pkg/commands/integration/slack/root.go | 31 +++ pkg/commands/integration/slack/slack_test.go | 126 ++++++++++ pkg/commands/integration/slack/update.go | 87 +++++++ .../splunkoncall/splunkoncall_test.go | 10 +- .../integration/splunkoncall/update.go | 2 +- pkg/commands/integration/webhook/create.go | 75 ++++++ pkg/commands/integration/webhook/doc.go | 3 + .../integration/webhook/get-signing-key.go | 63 +++++ pkg/commands/integration/webhook/root.go | 31 +++ .../integration/webhook/rotate-signing-key.go | 63 +++++ pkg/commands/integration/webhook/update.go | 87 +++++++ .../integration/webhook/webhook_test.go | 216 ++++++++++++++++++ pkg/mock/api.go | 30 ++- 49 files changed, 2451 insertions(+), 45 deletions(-) create mode 100644 pkg/commands/integration/mail/confirm.go create mode 100644 pkg/commands/integration/mail/create.go create mode 100644 pkg/commands/integration/mail/doc.go create mode 100644 pkg/commands/integration/mail/mail_test.go create mode 100644 pkg/commands/integration/mail/root.go create mode 100644 pkg/commands/integration/mail/update.go create mode 100644 pkg/commands/integration/msteams/create.go create mode 100644 pkg/commands/integration/msteams/doc.go create mode 100644 pkg/commands/integration/msteams/msteams_test.go create mode 100644 pkg/commands/integration/msteams/root.go create mode 100644 pkg/commands/integration/msteams/update.go create mode 100644 pkg/commands/integration/newrelic/create.go create mode 100644 pkg/commands/integration/newrelic/doc.go create mode 100644 pkg/commands/integration/newrelic/newrelic_test.go create mode 100644 pkg/commands/integration/newrelic/root.go create mode 100644 pkg/commands/integration/newrelic/update.go create mode 100644 pkg/commands/integration/pagerduty/create.go create mode 100644 pkg/commands/integration/pagerduty/doc.go create mode 100644 pkg/commands/integration/pagerduty/pagerduty_test.go create mode 100644 pkg/commands/integration/pagerduty/root.go create mode 100644 pkg/commands/integration/pagerduty/update.go create mode 100644 pkg/commands/integration/slack/create.go create mode 100644 pkg/commands/integration/slack/doc.go create mode 100644 pkg/commands/integration/slack/root.go create mode 100644 pkg/commands/integration/slack/slack_test.go create mode 100644 pkg/commands/integration/slack/update.go create mode 100644 pkg/commands/integration/webhook/create.go create mode 100644 pkg/commands/integration/webhook/doc.go create mode 100644 pkg/commands/integration/webhook/get-signing-key.go create mode 100644 pkg/commands/integration/webhook/root.go create mode 100644 pkg/commands/integration/webhook/rotate-signing-key.go create mode 100644 pkg/commands/integration/webhook/update.go create mode 100644 pkg/commands/integration/webhook/webhook_test.go diff --git a/pkg/api/interface.go b/pkg/api/interface.go index 488255088..af61d0e80 100644 --- a/pkg/api/interface.go +++ b/pkg/api/interface.go @@ -363,6 +363,9 @@ type Interface interface { UpdateIntegration(context.Context, *fastly.UpdateIntegrationInput) error DeleteIntegration(context.Context, *fastly.DeleteIntegrationInput) error GetIntegrationTypes(context.Context) (*[]fastly.IntegrationType, error) + GetWebhookSigningKey(context.Context, *fastly.GetWebhookSigningKeyInput) (*fastly.WebhookSigningKeyResponse, error) + RotateWebhookSigningKey(context.Context, *fastly.RotateWebhookSigningKeyInput) (*fastly.WebhookSigningKeyResponse, error) + CreateMailinglistConfirmation(context.Context, *fastly.CreateMailinglistConfirmationInput) error CreateConfigStore(context.Context, *fastly.CreateConfigStoreInput) (*fastly.ConfigStore, error) DeleteConfigStore(context.Context, *fastly.DeleteConfigStoreInput) error diff --git a/pkg/app/metadata.json b/pkg/app/metadata.json index a1278bb3b..a1cf18902 100644 --- a/pkg/app/metadata.json +++ b/pkg/app/metadata.json @@ -2938,6 +2938,93 @@ "https://www.fastly.com/documentation/reference/api/observability/notifications/#update-an-integration" ] } + }, + "mail": { + "create": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#create-an-integration" + ] + }, + "update": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#update-an-integration" + ] + }, + "confirm": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#create-a-mailing-list-confirmation" + ] + } + }, + "msteams": { + "create": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#create-an-integration" + ] + }, + "update": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#update-an-integration" + ] + } + }, + "newrelic": { + "create": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#create-an-integration" + ] + }, + "update": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#update-an-integration" + ] + } + }, + "pagerduty": { + "create": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#create-an-integration" + ] + }, + "update": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#update-an-integration" + ] + } + }, + "slack": { + "create": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#create-an-integration" + ] + }, + "update": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#update-an-integration" + ] + } + }, + "webhook": { + "create": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#create-an-integration" + ] + }, + "update": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#update-an-integration" + ] + }, + "get-signing-key": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#get-a-webhook-signing-key" + ] + }, + "rotate-signing-key": { + "apis": [ + "https://www.fastly.com/documentation/reference/api/observability/notifications/#rotate-a-webhook-signing-key" + ] + } } }, "kv-store": { diff --git a/pkg/argparser/common.go b/pkg/argparser/common.go index 198b2f9f2..5b84d2fee 100644 --- a/pkg/argparser/common.go +++ b/pkg/argparser/common.go @@ -65,13 +65,3 @@ func StoreIDFlag(dst *string) StringFlagOpts { Required: true, } } - -// IntegrationIDFlag returns an id flag definition for a notification integration. -func IntegrationIDFlag(dst *string) StringFlagOpts { - return StringFlagOpts{ - Name: "id", - Description: "Integration ID", - Dst: dst, - Required: true, - } -} diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index 99f559019..748b08496 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -73,8 +73,14 @@ import ( integrationDatadog "github.com/fastly/cli/pkg/commands/integration/datadog" integrationJiraIssue "github.com/fastly/cli/pkg/commands/integration/jiraissue" integrationJSM "github.com/fastly/cli/pkg/commands/integration/jsm" + integrationMail "github.com/fastly/cli/pkg/commands/integration/mail" + integrationMSTeams "github.com/fastly/cli/pkg/commands/integration/msteams" + integrationNewRelic "github.com/fastly/cli/pkg/commands/integration/newrelic" integrationOpsgenie "github.com/fastly/cli/pkg/commands/integration/opsgenie" + integrationPagerDuty "github.com/fastly/cli/pkg/commands/integration/pagerduty" + integrationSlack "github.com/fastly/cli/pkg/commands/integration/slack" integrationSplunkOnCall "github.com/fastly/cli/pkg/commands/integration/splunkoncall" + integrationWebhook "github.com/fastly/cli/pkg/commands/integration/webhook" "github.com/fastly/cli/pkg/commands/ip" "github.com/fastly/cli/pkg/commands/kvstore" "github.com/fastly/cli/pkg/commands/kvstoreentry" @@ -334,6 +340,27 @@ func Define( // nolint:revive // function-length integrationSplunkOnCallRoot := integrationSplunkOnCall.NewRootCommand(integrationRoot.CmdClause, data) integrationSplunkOnCallCreate := integrationSplunkOnCall.NewCreateCommand(integrationSplunkOnCallRoot.CmdClause, data) integrationSplunkOnCallUpdate := integrationSplunkOnCall.NewUpdateCommand(integrationSplunkOnCallRoot.CmdClause, data) + integrationMailRoot := integrationMail.NewRootCommand(integrationRoot.CmdClause, data) + integrationMailCreate := integrationMail.NewCreateCommand(integrationMailRoot.CmdClause, data) + integrationMailUpdate := integrationMail.NewUpdateCommand(integrationMailRoot.CmdClause, data) + integrationMailConfirm := integrationMail.NewConfirmCommand(integrationMailRoot.CmdClause, data) + integrationMSTeamsRoot := integrationMSTeams.NewRootCommand(integrationRoot.CmdClause, data) + integrationMSTeamsCreate := integrationMSTeams.NewCreateCommand(integrationMSTeamsRoot.CmdClause, data) + integrationMSTeamsUpdate := integrationMSTeams.NewUpdateCommand(integrationMSTeamsRoot.CmdClause, data) + integrationNewRelicRoot := integrationNewRelic.NewRootCommand(integrationRoot.CmdClause, data) + integrationNewRelicCreate := integrationNewRelic.NewCreateCommand(integrationNewRelicRoot.CmdClause, data) + integrationNewRelicUpdate := integrationNewRelic.NewUpdateCommand(integrationNewRelicRoot.CmdClause, data) + integrationPagerDutyRoot := integrationPagerDuty.NewRootCommand(integrationRoot.CmdClause, data) + integrationPagerDutyCreate := integrationPagerDuty.NewCreateCommand(integrationPagerDutyRoot.CmdClause, data) + integrationPagerDutyUpdate := integrationPagerDuty.NewUpdateCommand(integrationPagerDutyRoot.CmdClause, data) + integrationSlackRoot := integrationSlack.NewRootCommand(integrationRoot.CmdClause, data) + integrationSlackCreate := integrationSlack.NewCreateCommand(integrationSlackRoot.CmdClause, data) + integrationSlackUpdate := integrationSlack.NewUpdateCommand(integrationSlackRoot.CmdClause, data) + integrationWebhookRoot := integrationWebhook.NewRootCommand(integrationRoot.CmdClause, data) + integrationWebhookCreate := integrationWebhook.NewCreateCommand(integrationWebhookRoot.CmdClause, data) + integrationWebhookUpdate := integrationWebhook.NewUpdateCommand(integrationWebhookRoot.CmdClause, data) + integrationWebhookGetSigningKey := integrationWebhook.NewGetSigningKeyCommand(integrationWebhookRoot.CmdClause, data) + integrationWebhookRotateSigningKey := integrationWebhook.NewRotateSigningKeyCommand(integrationWebhookRoot.CmdClause, data) ipCmdRoot := ip.NewRootCommand(app, data) kvstoreCmdRoot := kvstore.NewRootCommand(app, data) kvstoreCreate := kvstore.NewCreateCommand(kvstoreCmdRoot.CmdClause, data) @@ -1246,6 +1273,27 @@ func Define( // nolint:revive // function-length integrationSplunkOnCallRoot, integrationSplunkOnCallCreate, integrationSplunkOnCallUpdate, + integrationMailRoot, + integrationMailCreate, + integrationMailUpdate, + integrationMailConfirm, + integrationMSTeamsRoot, + integrationMSTeamsCreate, + integrationMSTeamsUpdate, + integrationNewRelicRoot, + integrationNewRelicCreate, + integrationNewRelicUpdate, + integrationPagerDutyRoot, + integrationPagerDutyCreate, + integrationPagerDutyUpdate, + integrationSlackRoot, + integrationSlackCreate, + integrationSlackUpdate, + integrationWebhookRoot, + integrationWebhookCreate, + integrationWebhookUpdate, + integrationWebhookGetSigningKey, + integrationWebhookRotateSigningKey, ipCmdRoot, kvstoreCreate, kvstoreDelete, diff --git a/pkg/commands/integration/datadog/datadog_test.go b/pkg/commands/integration/datadog/datadog_test.go index 94ea97868..22127b42d 100644 --- a/pkg/commands/integration/datadog/datadog_test.go +++ b/pkg/commands/integration/datadog/datadog_test.go @@ -90,14 +90,14 @@ func TestUpdateCommand(t *testing.T) { scenarios := []testutil.CLIScenario{ { Args: fmt.Sprintf("--api-key %s", apiKey), - WantError: "error parsing arguments: required flag --id not provided", + WantError: "error parsing arguments: required argument 'id' not provided", }, { - Args: fmt.Sprintf("--id %s", integrationID), + Args: integrationID, WantError: "error parsing arguments: required flag --api-key not provided", }, { - Args: fmt.Sprintf("--id %s --api-key %s", integrationID, apiKey), + Args: fmt.Sprintf("%s --api-key %s", integrationID, apiKey), API: &mock.API{ UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { return errors.New("invalid request") @@ -106,7 +106,7 @@ func TestUpdateCommand(t *testing.T) { WantError: "invalid request", }, { - Args: fmt.Sprintf("--id %s --api-key %s", integrationID, apiKey), + Args: fmt.Sprintf("%s --api-key %s", integrationID, apiKey), API: &mock.API{ UpdateIntegrationFn: func(_ context.Context, i *fastly.UpdateIntegrationInput) error { if i.ID != integrationID { @@ -124,7 +124,7 @@ func TestUpdateCommand(t *testing.T) { WantOutput: fstfmt.Success("Updated Datadog integration (id: %s)", integrationID), }, { - Args: fmt.Sprintf("--id %s --api-key %s --json", integrationID, apiKey), + Args: fmt.Sprintf("%s --api-key %s --json", integrationID, apiKey), API: &mock.API{ UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { return nil diff --git a/pkg/commands/integration/datadog/update.go b/pkg/commands/integration/datadog/update.go index 84ab1e956..ff29ed8c6 100644 --- a/pkg/commands/integration/datadog/update.go +++ b/pkg/commands/integration/datadog/update.go @@ -37,7 +37,7 @@ func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateComman c.CmdClause = parent.Command("update", "Update a Datadog notification integration") // Required. - c.RegisterFlag(argparser.IntegrationIDFlag(&c.ID)) + c.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.ID) c.CmdClause.Flag("api-key", "Datadog API key").Required().StringVar(&c.APIKey) // Optional. diff --git a/pkg/commands/integration/jiraissue/jiraissue_test.go b/pkg/commands/integration/jiraissue/jiraissue_test.go index b28fe9078..5aa856aa8 100644 --- a/pkg/commands/integration/jiraissue/jiraissue_test.go +++ b/pkg/commands/integration/jiraissue/jiraissue_test.go @@ -73,10 +73,10 @@ func TestUpdateCommand(t *testing.T) { scenarios := []testutil.CLIScenario{ { Args: requiredFlags, - WantError: "error parsing arguments: required flag --id not provided", + WantError: "error parsing arguments: required argument 'id' not provided", }, { - Args: fmt.Sprintf("--id %s %s", integrationID, requiredFlags), + Args: fmt.Sprintf("%s %s", integrationID, requiredFlags), API: &mock.API{ UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { return errors.New("invalid request") @@ -85,7 +85,7 @@ func TestUpdateCommand(t *testing.T) { WantError: "invalid request", }, { - Args: fmt.Sprintf("--id %s %s", integrationID, requiredFlags), + Args: fmt.Sprintf("%s %s", integrationID, requiredFlags), API: &mock.API{ UpdateIntegrationFn: func(_ context.Context, i *fastly.UpdateIntegrationInput) error { if i.ID != integrationID { @@ -100,7 +100,7 @@ func TestUpdateCommand(t *testing.T) { WantOutput: fstfmt.Success("Updated Jira Issue integration (id: %s)", integrationID), }, { - Args: fmt.Sprintf("--id %s %s --json", integrationID, requiredFlags), + Args: fmt.Sprintf("%s %s --json", integrationID, requiredFlags), API: &mock.API{ UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { return nil diff --git a/pkg/commands/integration/jiraissue/update.go b/pkg/commands/integration/jiraissue/update.go index 0372c6ba7..bdb279290 100644 --- a/pkg/commands/integration/jiraissue/update.go +++ b/pkg/commands/integration/jiraissue/update.go @@ -40,7 +40,7 @@ func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateComman c.CmdClause = parent.Command("update", "Update a Jira Issue notification integration") // Required. - c.RegisterFlag(argparser.IntegrationIDFlag(&c.ID)) + c.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.ID) c.CmdClause.Flag("base-url", "The base URL of the Jira instance").Required().StringVar(&c.BaseURL) c.CmdClause.Flag("username", "The Jira username (email address) used to authenticate").Required().StringVar(&c.Username) c.CmdClause.Flag("api-token", "The Jira API token").Required().StringVar(&c.APIToken) diff --git a/pkg/commands/integration/jsm/jsm_test.go b/pkg/commands/integration/jsm/jsm_test.go index 42a35c6f7..1ad7610bb 100644 --- a/pkg/commands/integration/jsm/jsm_test.go +++ b/pkg/commands/integration/jsm/jsm_test.go @@ -78,14 +78,14 @@ func TestUpdateCommand(t *testing.T) { scenarios := []testutil.CLIScenario{ { Args: fmt.Sprintf("--api-key %s", apiKey), - WantError: "error parsing arguments: required flag --id not provided", + WantError: "error parsing arguments: required argument 'id' not provided", }, { - Args: fmt.Sprintf("--id %s", integrationID), + Args: integrationID, WantError: "error parsing arguments: required flag --api-key not provided", }, { - Args: fmt.Sprintf("--id %s --api-key %s", integrationID, apiKey), + Args: fmt.Sprintf("%s --api-key %s", integrationID, apiKey), API: &mock.API{ UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { return errors.New("invalid request") @@ -94,7 +94,7 @@ func TestUpdateCommand(t *testing.T) { WantError: "invalid request", }, { - Args: fmt.Sprintf("--id %s --api-key %s", integrationID, apiKey), + Args: fmt.Sprintf("%s --api-key %s", integrationID, apiKey), API: &mock.API{ UpdateIntegrationFn: func(_ context.Context, i *fastly.UpdateIntegrationInput) error { if i.ID != integrationID { @@ -112,7 +112,7 @@ func TestUpdateCommand(t *testing.T) { WantOutput: fstfmt.Success("Updated Jira Service Management integration (id: %s)", integrationID), }, { - Args: fmt.Sprintf("--id %s --api-key %s --json", integrationID, apiKey), + Args: fmt.Sprintf("%s --api-key %s --json", integrationID, apiKey), API: &mock.API{ UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { return nil diff --git a/pkg/commands/integration/jsm/update.go b/pkg/commands/integration/jsm/update.go index a9e08ab80..151f1e40d 100644 --- a/pkg/commands/integration/jsm/update.go +++ b/pkg/commands/integration/jsm/update.go @@ -36,7 +36,7 @@ func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateComman c.CmdClause = parent.Command("update", "Update a Jira Service Management notification integration") // Required. - c.RegisterFlag(argparser.IntegrationIDFlag(&c.ID)) + c.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.ID) c.CmdClause.Flag("api-key", "Jira Service Management API key").Required().StringVar(&c.APIKey) // Optional. diff --git a/pkg/commands/integration/list.go b/pkg/commands/integration/list.go index b4861a26c..458f3dae0 100644 --- a/pkg/commands/integration/list.go +++ b/pkg/commands/integration/list.go @@ -7,6 +7,12 @@ import ( "github.com/fastly/go-fastly/v17/fastly" "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/commands/integration/mail" + "github.com/fastly/cli/pkg/commands/integration/msteams" + "github.com/fastly/cli/pkg/commands/integration/newrelic" + "github.com/fastly/cli/pkg/commands/integration/pagerduty" + "github.com/fastly/cli/pkg/commands/integration/slack" + "github.com/fastly/cli/pkg/commands/integration/webhook" fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/global" "github.com/fastly/cli/pkg/text" @@ -21,6 +27,12 @@ var knownIntegrationTypes = []string{ fastly.IntegrationTypeJSM, fastly.IntegrationTypeOpsGenie, fastly.IntegrationTypeSplunkOnCall, + mail.CommandName, + msteams.CommandName, + newrelic.CommandName, + pagerduty.CommandName, + slack.CommandName, + webhook.CommandName, } // ListCommand calls the Fastly API to list notification integrations. diff --git a/pkg/commands/integration/mail/confirm.go b/pkg/commands/integration/mail/confirm.go new file mode 100644 index 000000000..b52b52690 --- /dev/null +++ b/pkg/commands/integration/mail/confirm.go @@ -0,0 +1,68 @@ +package mail + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// ConfirmCommand calls the Fastly API to send a mailing list confirmation email. +type ConfirmCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + Address string +} + +// NewConfirmCommand returns a usable command registered under the parent. +func NewConfirmCommand(parent argparser.Registerer, g *global.Data) *ConfirmCommand { + c := ConfirmCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("confirm", "Send a mailing list confirmation email") + + // Required. + c.CmdClause.Arg("address", "The mailing list address to send the confirmation email to").Required().StringVar(&c.Address) + + // Optional. + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *ConfirmCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + input := &fastly.CreateMailinglistConfirmationInput{Email: &c.Address} + if err := c.Globals.APIClient.CreateMailinglistConfirmation(context.TODO(), input); err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if c.JSONOutput.Enabled { + o := struct { + Address string `json:"address"` + Sent bool `json:"sent"` + }{ + c.Address, + true, + } + _, err := c.WriteJSON(out, o) + return err + } + + text.Success(out, "Sent confirmation email to '%s'", c.Address) + return nil +} diff --git a/pkg/commands/integration/mail/create.go b/pkg/commands/integration/mail/create.go new file mode 100644 index 000000000..51949dacb --- /dev/null +++ b/pkg/commands/integration/mail/create.go @@ -0,0 +1,75 @@ +package mail + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// CreateCommand calls the Fastly API to create a Mailing List notification integration. +type CreateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + IntegrationName string + Address string + + // Optional. + Description argparser.OptionalString +} + +// NewCreateCommand returns a usable command registered under the parent. +func NewCreateCommand(parent argparser.Registerer, g *global.Data) *CreateCommand { + c := CreateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("create", "Create a Mailing List notification integration").Alias("add") + + // Required. + c.CmdClause.Flag("name", "The name of the integration").Short('n').Required().StringVar(&c.IntegrationName) + c.CmdClause.Flag("address", "The mailing list address to notify").Required().StringVar(&c.Address) + + // Optional. + c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *CreateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + input := &fastly.CreateIntegrationInput{ + Name: &c.IntegrationName, + Type: fastly.ToPointer(CommandName), + Config: map[string]string{"address": c.Address}, + } + if c.Description.WasSet { + input.Description = &c.Description.Value + } + + o, err := c.Globals.APIClient.CreateIntegration(context.TODO(), input) + if err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if ok, err := c.WriteJSON(out, o); ok { + return err + } + + text.Success(out, "Created Mailing List integration '%s' (id: %s)", c.IntegrationName, fastly.ToValue(o.ID)) + return nil +} diff --git a/pkg/commands/integration/mail/doc.go b/pkg/commands/integration/mail/doc.go new file mode 100644 index 000000000..61ddea506 --- /dev/null +++ b/pkg/commands/integration/mail/doc.go @@ -0,0 +1,3 @@ +// Package mail contains commands to manage Mailing List notification +// integrations. +package mail diff --git a/pkg/commands/integration/mail/mail_test.go b/pkg/commands/integration/mail/mail_test.go new file mode 100644 index 000000000..2357591b5 --- /dev/null +++ b/pkg/commands/integration/mail/mail_test.go @@ -0,0 +1,168 @@ +package mail_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/fastly/go-fastly/v17/fastly" + + root "github.com/fastly/cli/pkg/commands/integration" + sub "github.com/fastly/cli/pkg/commands/integration/mail" + fstfmt "github.com/fastly/cli/pkg/fmt" + "github.com/fastly/cli/pkg/mock" + "github.com/fastly/cli/pkg/testutil" +) + +func TestCreateCommand(t *testing.T) { + const ( + integrationName = "test123" + integrationID = "integration-id-123" + address = "alerts@example.com" + ) + + scenarios := []testutil.CLIScenario{ + { + Args: fmt.Sprintf("--address %s", address), + WantError: "error parsing arguments: required flag --name not provided", + }, + { + Args: fmt.Sprintf("--name %s", integrationName), + WantError: "error parsing arguments: required flag --address not provided", + }, + { + Args: fmt.Sprintf("--name %s --address %s", integrationName, address), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, _ *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return nil, errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: fmt.Sprintf("--name %s --address %s", integrationName, address), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, i *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + if fastly.ToValue(i.Type) != sub.CommandName { + return nil, fmt.Errorf("unexpected type: %s", fastly.ToValue(i.Type)) + } + if i.Config["address"] != address { + return nil, fmt.Errorf("unexpected address: %s", i.Config["address"]) + } + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.Success("Created Mailing List integration '%s' (id: %s)", integrationName, integrationID), + }, + { + Args: fmt.Sprintf("--name %s --address %s --json", integrationName, address), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, _ *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.EncodeJSON(&fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "create"}, scenarios) +} + +func TestUpdateCommand(t *testing.T) { + const ( + integrationID = "integration-id-123" + address = "alerts@example.com" + ) + + scenarios := []testutil.CLIScenario{ + { + Args: fmt.Sprintf("--address %s", address), + WantError: "error parsing arguments: required argument 'id' not provided", + }, + { + Args: integrationID, + WantError: "error parsing arguments: required flag --address not provided", + }, + { + Args: fmt.Sprintf("%s --address %s", integrationID, address), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { + return errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: fmt.Sprintf("%s --address %s", integrationID, address), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, i *fastly.UpdateIntegrationInput) error { + if i.ID != integrationID { + return fmt.Errorf("unexpected id: %s", i.ID) + } + if fastly.ToValue(i.Type) != sub.CommandName { + return fmt.Errorf("unexpected type: %s", fastly.ToValue(i.Type)) + } + if i.Config["address"] != address { + return fmt.Errorf("unexpected address: %s", i.Config["address"]) + } + return nil + }, + }, + WantOutput: fstfmt.Success("Updated Mailing List integration (id: %s)", integrationID), + }, + { + Args: fmt.Sprintf("%s --address %s --json", integrationID, address), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { + return nil + }, + }, + WantOutput: fstfmt.JSON(`{"id": %q, "updated": true}`, integrationID), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "update"}, scenarios) +} + +func TestConfirmCommand(t *testing.T) { + const address = "alerts@example.com" + + scenarios := []testutil.CLIScenario{ + { + WantError: "error parsing arguments: required argument 'address' not provided", + }, + { + Args: address, + API: &mock.API{ + CreateMailinglistConfirmationFn: func(_ context.Context, _ *fastly.CreateMailinglistConfirmationInput) error { + return errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: address, + API: &mock.API{ + CreateMailinglistConfirmationFn: func(_ context.Context, i *fastly.CreateMailinglistConfirmationInput) error { + if fastly.ToValue(i.Email) != address { + return fmt.Errorf("unexpected address: %s", fastly.ToValue(i.Email)) + } + return nil + }, + }, + WantOutput: fstfmt.Success("Sent confirmation email to '%s'", address), + }, + { + Args: fmt.Sprintf("%s --json", address), + API: &mock.API{ + CreateMailinglistConfirmationFn: func(_ context.Context, _ *fastly.CreateMailinglistConfirmationInput) error { + return nil + }, + }, + WantOutput: fstfmt.JSON(`{"address": %q, "sent": true}`, address), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "confirm"}, scenarios) +} diff --git a/pkg/commands/integration/mail/root.go b/pkg/commands/integration/mail/root.go new file mode 100644 index 000000000..38aca06e6 --- /dev/null +++ b/pkg/commands/integration/mail/root.go @@ -0,0 +1,31 @@ +package mail + +import ( + "io" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/global" +) + +// CommandName is the string to be used to invoke this command. +const CommandName = "mail" + +// 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 +} + +// 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, "Manage Mailing List notification integrations") + return &c +} + +// Exec implements the command interface. +func (c *RootCommand) Exec(_ io.Reader, _ io.Writer) error { + panic("unreachable") +} diff --git a/pkg/commands/integration/mail/update.go b/pkg/commands/integration/mail/update.go new file mode 100644 index 000000000..bf657db50 --- /dev/null +++ b/pkg/commands/integration/mail/update.go @@ -0,0 +1,87 @@ +package mail + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// UpdateCommand calls the Fastly API to update a Mailing List notification integration. +type UpdateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + ID string + Address string + + // Optional. + IntegrationName argparser.OptionalString + Description argparser.OptionalString +} + +// NewUpdateCommand returns a usable command registered under the parent. +func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateCommand { + c := UpdateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("update", "Update a Mailing List notification integration") + + // Required. + c.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.ID) + c.CmdClause.Flag("address", "The mailing list address to notify").Required().StringVar(&c.Address) + + // Optional. + c.CmdClause.Flag("name", "The name of the integration").Short('n').Action(c.IntegrationName.Set).StringVar(&c.IntegrationName.Value) + c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *UpdateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + input := &fastly.UpdateIntegrationInput{ + ID: c.ID, + Type: fastly.ToPointer(CommandName), + Config: map[string]string{"address": c.Address}, + } + if c.IntegrationName.WasSet { + input.Name = &c.IntegrationName.Value + } + if c.Description.WasSet { + input.Description = &c.Description.Value + } + + if err := c.Globals.APIClient.UpdateIntegration(context.TODO(), input); err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if c.JSONOutput.Enabled { + o := struct { + ID string `json:"id"` + Updated bool `json:"updated"` + }{ + c.ID, + true, + } + _, err := c.WriteJSON(out, o) + return err + } + + text.Success(out, "Updated Mailing List integration (id: %s)", c.ID) + return nil +} diff --git a/pkg/commands/integration/msteams/create.go b/pkg/commands/integration/msteams/create.go new file mode 100644 index 000000000..e61ad78f0 --- /dev/null +++ b/pkg/commands/integration/msteams/create.go @@ -0,0 +1,75 @@ +package msteams + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// CreateCommand calls the Fastly API to create a Microsoft Teams notification integration. +type CreateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + IntegrationName string + Webhook string + + // Optional. + Description argparser.OptionalString +} + +// NewCreateCommand returns a usable command registered under the parent. +func NewCreateCommand(parent argparser.Registerer, g *global.Data) *CreateCommand { + c := CreateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("create", "Create a Microsoft Teams notification integration").Alias("add") + + // Required. + c.CmdClause.Flag("name", "The name of the integration").Short('n').Required().StringVar(&c.IntegrationName) + c.CmdClause.Flag("webhook", "The Microsoft Teams incoming webhook URL").Required().StringVar(&c.Webhook) + + // Optional. + c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *CreateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + input := &fastly.CreateIntegrationInput{ + Name: &c.IntegrationName, + Type: fastly.ToPointer(CommandName), + Config: map[string]string{"webhook": c.Webhook}, + } + if c.Description.WasSet { + input.Description = &c.Description.Value + } + + o, err := c.Globals.APIClient.CreateIntegration(context.TODO(), input) + if err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if ok, err := c.WriteJSON(out, o); ok { + return err + } + + text.Success(out, "Created Microsoft Teams integration '%s' (id: %s)", c.IntegrationName, fastly.ToValue(o.ID)) + return nil +} diff --git a/pkg/commands/integration/msteams/doc.go b/pkg/commands/integration/msteams/doc.go new file mode 100644 index 000000000..9bb393c22 --- /dev/null +++ b/pkg/commands/integration/msteams/doc.go @@ -0,0 +1,3 @@ +// Package msteams contains commands to manage Microsoft Teams notification +// integrations. +package msteams diff --git a/pkg/commands/integration/msteams/msteams_test.go b/pkg/commands/integration/msteams/msteams_test.go new file mode 100644 index 000000000..a1e7210c4 --- /dev/null +++ b/pkg/commands/integration/msteams/msteams_test.go @@ -0,0 +1,126 @@ +package msteams_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/fastly/go-fastly/v17/fastly" + + root "github.com/fastly/cli/pkg/commands/integration" + sub "github.com/fastly/cli/pkg/commands/integration/msteams" + fstfmt "github.com/fastly/cli/pkg/fmt" + "github.com/fastly/cli/pkg/mock" + "github.com/fastly/cli/pkg/testutil" +) + +func TestCreateCommand(t *testing.T) { + const ( + integrationName = "test123" + integrationID = "integration-id-123" + webhookURL = "https://outlook.office.com/webhook/xyz" + ) + + scenarios := []testutil.CLIScenario{ + { + Args: fmt.Sprintf("--webhook %s", webhookURL), + WantError: "error parsing arguments: required flag --name not provided", + }, + { + Args: fmt.Sprintf("--name %s", integrationName), + WantError: "error parsing arguments: required flag --webhook not provided", + }, + { + Args: fmt.Sprintf("--name %s --webhook %s", integrationName, webhookURL), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, _ *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return nil, errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: fmt.Sprintf("--name %s --webhook %s", integrationName, webhookURL), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, i *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + if fastly.ToValue(i.Type) != sub.CommandName { + return nil, fmt.Errorf("unexpected type: %s", fastly.ToValue(i.Type)) + } + if i.Config["webhook"] != webhookURL { + return nil, fmt.Errorf("unexpected webhook: %s", i.Config["webhook"]) + } + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.Success("Created Microsoft Teams integration '%s' (id: %s)", integrationName, integrationID), + }, + { + Args: fmt.Sprintf("--name %s --webhook %s --json", integrationName, webhookURL), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, _ *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.EncodeJSON(&fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "create"}, scenarios) +} + +func TestUpdateCommand(t *testing.T) { + const ( + integrationID = "integration-id-123" + webhookURL = "https://outlook.office.com/webhook/xyz" + ) + + scenarios := []testutil.CLIScenario{ + { + Args: fmt.Sprintf("--webhook %s", webhookURL), + WantError: "error parsing arguments: required argument 'id' not provided", + }, + { + Args: integrationID, + WantError: "error parsing arguments: required flag --webhook not provided", + }, + { + Args: fmt.Sprintf("%s --webhook %s", integrationID, webhookURL), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { + return errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: fmt.Sprintf("%s --webhook %s", integrationID, webhookURL), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, i *fastly.UpdateIntegrationInput) error { + if i.ID != integrationID { + return fmt.Errorf("unexpected id: %s", i.ID) + } + if fastly.ToValue(i.Type) != sub.CommandName { + return fmt.Errorf("unexpected type: %s", fastly.ToValue(i.Type)) + } + if i.Config["webhook"] != webhookURL { + return fmt.Errorf("unexpected webhook: %s", i.Config["webhook"]) + } + return nil + }, + }, + WantOutput: fstfmt.Success("Updated Microsoft Teams integration (id: %s)", integrationID), + }, + { + Args: fmt.Sprintf("%s --webhook %s --json", integrationID, webhookURL), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { + return nil + }, + }, + WantOutput: fstfmt.JSON(`{"id": %q, "updated": true}`, integrationID), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "update"}, scenarios) +} diff --git a/pkg/commands/integration/msteams/root.go b/pkg/commands/integration/msteams/root.go new file mode 100644 index 000000000..e62970f9c --- /dev/null +++ b/pkg/commands/integration/msteams/root.go @@ -0,0 +1,31 @@ +package msteams + +import ( + "io" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/global" +) + +// CommandName is the string to be used to invoke this command. +const CommandName = "msteams" + +// 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 +} + +// 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, "Manage Microsoft Teams notification integrations") + return &c +} + +// Exec implements the command interface. +func (c *RootCommand) Exec(_ io.Reader, _ io.Writer) error { + panic("unreachable") +} diff --git a/pkg/commands/integration/msteams/update.go b/pkg/commands/integration/msteams/update.go new file mode 100644 index 000000000..510efbc41 --- /dev/null +++ b/pkg/commands/integration/msteams/update.go @@ -0,0 +1,87 @@ +package msteams + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// UpdateCommand calls the Fastly API to update a Microsoft Teams notification integration. +type UpdateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + ID string + Webhook string + + // Optional. + IntegrationName argparser.OptionalString + Description argparser.OptionalString +} + +// NewUpdateCommand returns a usable command registered under the parent. +func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateCommand { + c := UpdateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("update", "Update a Microsoft Teams notification integration") + + // Required. + c.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.ID) + c.CmdClause.Flag("webhook", "The Microsoft Teams incoming webhook URL").Required().StringVar(&c.Webhook) + + // Optional. + c.CmdClause.Flag("name", "The name of the integration").Short('n').Action(c.IntegrationName.Set).StringVar(&c.IntegrationName.Value) + c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *UpdateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + input := &fastly.UpdateIntegrationInput{ + ID: c.ID, + Type: fastly.ToPointer(CommandName), + Config: map[string]string{"webhook": c.Webhook}, + } + if c.IntegrationName.WasSet { + input.Name = &c.IntegrationName.Value + } + if c.Description.WasSet { + input.Description = &c.Description.Value + } + + if err := c.Globals.APIClient.UpdateIntegration(context.TODO(), input); err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if c.JSONOutput.Enabled { + o := struct { + ID string `json:"id"` + Updated bool `json:"updated"` + }{ + c.ID, + true, + } + _, err := c.WriteJSON(out, o) + return err + } + + text.Success(out, "Updated Microsoft Teams integration (id: %s)", c.ID) + return nil +} diff --git a/pkg/commands/integration/newrelic/create.go b/pkg/commands/integration/newrelic/create.go new file mode 100644 index 000000000..67912f824 --- /dev/null +++ b/pkg/commands/integration/newrelic/create.go @@ -0,0 +1,77 @@ +package newrelic + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// CreateCommand calls the Fastly API to create a New Relic notification integration. +type CreateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + IntegrationName string + Account string + APIKey string + + // Optional. + Description argparser.OptionalString +} + +// NewCreateCommand returns a usable command registered under the parent. +func NewCreateCommand(parent argparser.Registerer, g *global.Data) *CreateCommand { + c := CreateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("create", "Create a New Relic notification integration").Alias("add") + + // Required. + c.CmdClause.Flag("name", "The name of the integration").Short('n').Required().StringVar(&c.IntegrationName) + c.CmdClause.Flag("account-id", "The New Relic account ID").Required().StringVar(&c.Account) + c.CmdClause.Flag("api-key", "The New Relic API key").Required().StringVar(&c.APIKey) + + // Optional. + c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *CreateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + input := &fastly.CreateIntegrationInput{ + Name: &c.IntegrationName, + Type: fastly.ToPointer(CommandName), + Config: map[string]string{"account": c.Account, "key": c.APIKey}, + } + if c.Description.WasSet { + input.Description = &c.Description.Value + } + + o, err := c.Globals.APIClient.CreateIntegration(context.TODO(), input) + if err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if ok, err := c.WriteJSON(out, o); ok { + return err + } + + text.Success(out, "Created New Relic integration '%s' (id: %s)", c.IntegrationName, fastly.ToValue(o.ID)) + return nil +} diff --git a/pkg/commands/integration/newrelic/doc.go b/pkg/commands/integration/newrelic/doc.go new file mode 100644 index 000000000..41d017322 --- /dev/null +++ b/pkg/commands/integration/newrelic/doc.go @@ -0,0 +1,3 @@ +// Package newrelic contains commands to manage New Relic notification +// integrations. +package newrelic diff --git a/pkg/commands/integration/newrelic/newrelic_test.go b/pkg/commands/integration/newrelic/newrelic_test.go new file mode 100644 index 000000000..ca8a212d3 --- /dev/null +++ b/pkg/commands/integration/newrelic/newrelic_test.go @@ -0,0 +1,113 @@ +package newrelic_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/fastly/go-fastly/v17/fastly" + + root "github.com/fastly/cli/pkg/commands/integration" + sub "github.com/fastly/cli/pkg/commands/integration/newrelic" + fstfmt "github.com/fastly/cli/pkg/fmt" + "github.com/fastly/cli/pkg/mock" + "github.com/fastly/cli/pkg/testutil" +) + +const requiredFlags = "--account-id acct123 --api-key key123" + +func TestCreateCommand(t *testing.T) { + const ( + integrationName = "test123" + integrationID = "integration-id-123" + ) + + scenarios := []testutil.CLIScenario{ + { + Args: requiredFlags, + WantError: "error parsing arguments: required flag --name not provided", + }, + { + Args: fmt.Sprintf("--name %s %s", integrationName, requiredFlags), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, _ *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return nil, errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: fmt.Sprintf("--name %s %s", integrationName, requiredFlags), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, i *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + if fastly.ToValue(i.Type) != sub.CommandName { + return nil, fmt.Errorf("unexpected type: %s", fastly.ToValue(i.Type)) + } + if i.Config["account"] != "acct123" || i.Config["key"] != "key123" { + return nil, fmt.Errorf("unexpected config: %+v", i.Config) + } + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.Success("Created New Relic integration '%s' (id: %s)", integrationName, integrationID), + }, + { + Args: fmt.Sprintf("--name %s %s --json", integrationName, requiredFlags), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, _ *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.EncodeJSON(&fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "create"}, scenarios) +} + +func TestUpdateCommand(t *testing.T) { + const integrationID = "integration-id-123" + + scenarios := []testutil.CLIScenario{ + { + Args: requiredFlags, + WantError: "error parsing arguments: required argument 'id' not provided", + }, + { + Args: fmt.Sprintf("%s %s", integrationID, requiredFlags), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { + return errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: fmt.Sprintf("%s %s", integrationID, requiredFlags), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, i *fastly.UpdateIntegrationInput) error { + if i.ID != integrationID { + return fmt.Errorf("unexpected id: %s", i.ID) + } + if fastly.ToValue(i.Type) != sub.CommandName { + return fmt.Errorf("unexpected type: %s", fastly.ToValue(i.Type)) + } + return nil + }, + }, + WantOutput: fstfmt.Success("Updated New Relic integration (id: %s)", integrationID), + }, + { + Args: fmt.Sprintf("%s %s --json", integrationID, requiredFlags), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { + return nil + }, + }, + WantOutput: fstfmt.JSON(`{"id": %q, "updated": true}`, integrationID), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "update"}, scenarios) +} diff --git a/pkg/commands/integration/newrelic/root.go b/pkg/commands/integration/newrelic/root.go new file mode 100644 index 000000000..210450b5f --- /dev/null +++ b/pkg/commands/integration/newrelic/root.go @@ -0,0 +1,31 @@ +package newrelic + +import ( + "io" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/global" +) + +// CommandName is the string to be used to invoke this command. +const CommandName = "newrelic" + +// 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 +} + +// 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, "Manage New Relic notification integrations") + return &c +} + +// Exec implements the command interface. +func (c *RootCommand) Exec(_ io.Reader, _ io.Writer) error { + panic("unreachable") +} diff --git a/pkg/commands/integration/newrelic/update.go b/pkg/commands/integration/newrelic/update.go new file mode 100644 index 000000000..749c035b1 --- /dev/null +++ b/pkg/commands/integration/newrelic/update.go @@ -0,0 +1,89 @@ +package newrelic + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// UpdateCommand calls the Fastly API to update a New Relic notification integration. +type UpdateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + ID string + Account string + APIKey string + + // Optional. + IntegrationName argparser.OptionalString + Description argparser.OptionalString +} + +// NewUpdateCommand returns a usable command registered under the parent. +func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateCommand { + c := UpdateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("update", "Update a New Relic notification integration") + + // Required. + c.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.ID) + c.CmdClause.Flag("account-id", "The New Relic account ID").Required().StringVar(&c.Account) + c.CmdClause.Flag("api-key", "The New Relic API key").Required().StringVar(&c.APIKey) + + // Optional. + c.CmdClause.Flag("name", "The name of the integration").Short('n').Action(c.IntegrationName.Set).StringVar(&c.IntegrationName.Value) + c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *UpdateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + input := &fastly.UpdateIntegrationInput{ + ID: c.ID, + Type: fastly.ToPointer(CommandName), + Config: map[string]string{"account": c.Account, "key": c.APIKey}, + } + if c.IntegrationName.WasSet { + input.Name = &c.IntegrationName.Value + } + if c.Description.WasSet { + input.Description = &c.Description.Value + } + + if err := c.Globals.APIClient.UpdateIntegration(context.TODO(), input); err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if c.JSONOutput.Enabled { + o := struct { + ID string `json:"id"` + Updated bool `json:"updated"` + }{ + c.ID, + true, + } + _, err := c.WriteJSON(out, o) + return err + } + + text.Success(out, "Updated New Relic integration (id: %s)", c.ID) + return nil +} diff --git a/pkg/commands/integration/opsgenie/opsgenie_test.go b/pkg/commands/integration/opsgenie/opsgenie_test.go index 5484eeefd..f3123b012 100644 --- a/pkg/commands/integration/opsgenie/opsgenie_test.go +++ b/pkg/commands/integration/opsgenie/opsgenie_test.go @@ -78,14 +78,14 @@ func TestUpdateCommand(t *testing.T) { scenarios := []testutil.CLIScenario{ { Args: fmt.Sprintf("--api-key %s", apiKey), - WantError: "error parsing arguments: required flag --id not provided", + WantError: "error parsing arguments: required argument 'id' not provided", }, { - Args: fmt.Sprintf("--id %s", integrationID), + Args: integrationID, WantError: "error parsing arguments: required flag --api-key not provided", }, { - Args: fmt.Sprintf("--id %s --api-key %s", integrationID, apiKey), + Args: fmt.Sprintf("%s --api-key %s", integrationID, apiKey), API: &mock.API{ UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { return errors.New("invalid request") @@ -94,7 +94,7 @@ func TestUpdateCommand(t *testing.T) { WantError: "invalid request", }, { - Args: fmt.Sprintf("--id %s --api-key %s", integrationID, apiKey), + Args: fmt.Sprintf("%s --api-key %s", integrationID, apiKey), API: &mock.API{ UpdateIntegrationFn: func(_ context.Context, i *fastly.UpdateIntegrationInput) error { if i.ID != integrationID { @@ -112,7 +112,7 @@ func TestUpdateCommand(t *testing.T) { WantOutput: fstfmt.Success("Updated OpsGenie integration (id: %s)", integrationID), }, { - Args: fmt.Sprintf("--id %s --api-key %s --json", integrationID, apiKey), + Args: fmt.Sprintf("%s --api-key %s --json", integrationID, apiKey), API: &mock.API{ UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { return nil diff --git a/pkg/commands/integration/opsgenie/update.go b/pkg/commands/integration/opsgenie/update.go index d577fe00f..4d00c9a68 100644 --- a/pkg/commands/integration/opsgenie/update.go +++ b/pkg/commands/integration/opsgenie/update.go @@ -36,7 +36,7 @@ func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateComman c.CmdClause = parent.Command("update", "Update an OpsGenie notification integration") // Required. - c.RegisterFlag(argparser.IntegrationIDFlag(&c.ID)) + c.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.ID) c.CmdClause.Flag("api-key", "OpsGenie API key").Required().StringVar(&c.APIKey) // Optional. diff --git a/pkg/commands/integration/pagerduty/create.go b/pkg/commands/integration/pagerduty/create.go new file mode 100644 index 000000000..3aeeceeed --- /dev/null +++ b/pkg/commands/integration/pagerduty/create.go @@ -0,0 +1,75 @@ +package pagerduty + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// CreateCommand calls the Fastly API to create a PagerDuty notification integration. +type CreateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + IntegrationName string + IntegrationKey string + + // Optional. + Description argparser.OptionalString +} + +// NewCreateCommand returns a usable command registered under the parent. +func NewCreateCommand(parent argparser.Registerer, g *global.Data) *CreateCommand { + c := CreateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("create", "Create a PagerDuty notification integration").Alias("add") + + // Required. + c.CmdClause.Flag("name", "The name of the integration").Short('n').Required().StringVar(&c.IntegrationName) + c.CmdClause.Flag("key", "The PagerDuty integration key").Required().StringVar(&c.IntegrationKey) + + // Optional. + c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *CreateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + input := &fastly.CreateIntegrationInput{ + Name: &c.IntegrationName, + Type: fastly.ToPointer(CommandName), + Config: map[string]string{"key": c.IntegrationKey}, + } + if c.Description.WasSet { + input.Description = &c.Description.Value + } + + o, err := c.Globals.APIClient.CreateIntegration(context.TODO(), input) + if err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if ok, err := c.WriteJSON(out, o); ok { + return err + } + + text.Success(out, "Created PagerDuty integration '%s' (id: %s)", c.IntegrationName, fastly.ToValue(o.ID)) + return nil +} diff --git a/pkg/commands/integration/pagerduty/doc.go b/pkg/commands/integration/pagerduty/doc.go new file mode 100644 index 000000000..ae649aaaf --- /dev/null +++ b/pkg/commands/integration/pagerduty/doc.go @@ -0,0 +1,3 @@ +// Package pagerduty contains commands to manage PagerDuty notification +// integrations. +package pagerduty diff --git a/pkg/commands/integration/pagerduty/pagerduty_test.go b/pkg/commands/integration/pagerduty/pagerduty_test.go new file mode 100644 index 000000000..504552c3c --- /dev/null +++ b/pkg/commands/integration/pagerduty/pagerduty_test.go @@ -0,0 +1,126 @@ +package pagerduty_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/fastly/go-fastly/v17/fastly" + + root "github.com/fastly/cli/pkg/commands/integration" + sub "github.com/fastly/cli/pkg/commands/integration/pagerduty" + fstfmt "github.com/fastly/cli/pkg/fmt" + "github.com/fastly/cli/pkg/mock" + "github.com/fastly/cli/pkg/testutil" +) + +func TestCreateCommand(t *testing.T) { + const ( + integrationName = "test123" + integrationID = "integration-id-123" + integrationKey = "a1b2c3d4" + ) + + scenarios := []testutil.CLIScenario{ + { + Args: fmt.Sprintf("--key %s", integrationKey), + WantError: "error parsing arguments: required flag --name not provided", + }, + { + Args: fmt.Sprintf("--name %s", integrationName), + WantError: "error parsing arguments: required flag --key not provided", + }, + { + Args: fmt.Sprintf("--name %s --key %s", integrationName, integrationKey), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, _ *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return nil, errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: fmt.Sprintf("--name %s --key %s", integrationName, integrationKey), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, i *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + if fastly.ToValue(i.Type) != sub.CommandName { + return nil, fmt.Errorf("unexpected type: %s", fastly.ToValue(i.Type)) + } + if i.Config["key"] != integrationKey { + return nil, fmt.Errorf("unexpected key: %s", i.Config["key"]) + } + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.Success("Created PagerDuty integration '%s' (id: %s)", integrationName, integrationID), + }, + { + Args: fmt.Sprintf("--name %s --key %s --json", integrationName, integrationKey), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, _ *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.EncodeJSON(&fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "create"}, scenarios) +} + +func TestUpdateCommand(t *testing.T) { + const ( + integrationID = "integration-id-123" + integrationKey = "a1b2c3d4" + ) + + scenarios := []testutil.CLIScenario{ + { + Args: fmt.Sprintf("--key %s", integrationKey), + WantError: "error parsing arguments: required argument 'id' not provided", + }, + { + Args: integrationID, + WantError: "error parsing arguments: required flag --key not provided", + }, + { + Args: fmt.Sprintf("%s --key %s", integrationID, integrationKey), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { + return errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: fmt.Sprintf("%s --key %s", integrationID, integrationKey), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, i *fastly.UpdateIntegrationInput) error { + if i.ID != integrationID { + return fmt.Errorf("unexpected id: %s", i.ID) + } + if fastly.ToValue(i.Type) != sub.CommandName { + return fmt.Errorf("unexpected type: %s", fastly.ToValue(i.Type)) + } + if i.Config["key"] != integrationKey { + return fmt.Errorf("unexpected key: %s", i.Config["key"]) + } + return nil + }, + }, + WantOutput: fstfmt.Success("Updated PagerDuty integration (id: %s)", integrationID), + }, + { + Args: fmt.Sprintf("%s --key %s --json", integrationID, integrationKey), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { + return nil + }, + }, + WantOutput: fstfmt.JSON(`{"id": %q, "updated": true}`, integrationID), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "update"}, scenarios) +} diff --git a/pkg/commands/integration/pagerduty/root.go b/pkg/commands/integration/pagerduty/root.go new file mode 100644 index 000000000..6cdff535c --- /dev/null +++ b/pkg/commands/integration/pagerduty/root.go @@ -0,0 +1,31 @@ +package pagerduty + +import ( + "io" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/global" +) + +// CommandName is the string to be used to invoke this command. +const CommandName = "pagerduty" + +// 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 +} + +// 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, "Manage PagerDuty notification integrations") + return &c +} + +// Exec implements the command interface. +func (c *RootCommand) Exec(_ io.Reader, _ io.Writer) error { + panic("unreachable") +} diff --git a/pkg/commands/integration/pagerduty/update.go b/pkg/commands/integration/pagerduty/update.go new file mode 100644 index 000000000..43eebe658 --- /dev/null +++ b/pkg/commands/integration/pagerduty/update.go @@ -0,0 +1,87 @@ +package pagerduty + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// UpdateCommand calls the Fastly API to update a PagerDuty notification integration. +type UpdateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + ID string + IntegrationKey string + + // Optional. + IntegrationName argparser.OptionalString + Description argparser.OptionalString +} + +// NewUpdateCommand returns a usable command registered under the parent. +func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateCommand { + c := UpdateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("update", "Update a PagerDuty notification integration") + + // Required. + c.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.ID) + c.CmdClause.Flag("key", "The PagerDuty integration key").Required().StringVar(&c.IntegrationKey) + + // Optional. + c.CmdClause.Flag("name", "The name of the integration").Short('n').Action(c.IntegrationName.Set).StringVar(&c.IntegrationName.Value) + c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *UpdateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + input := &fastly.UpdateIntegrationInput{ + ID: c.ID, + Type: fastly.ToPointer(CommandName), + Config: map[string]string{"key": c.IntegrationKey}, + } + if c.IntegrationName.WasSet { + input.Name = &c.IntegrationName.Value + } + if c.Description.WasSet { + input.Description = &c.Description.Value + } + + if err := c.Globals.APIClient.UpdateIntegration(context.TODO(), input); err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if c.JSONOutput.Enabled { + o := struct { + ID string `json:"id"` + Updated bool `json:"updated"` + }{ + c.ID, + true, + } + _, err := c.WriteJSON(out, o) + return err + } + + text.Success(out, "Updated PagerDuty integration (id: %s)", c.ID) + return nil +} diff --git a/pkg/commands/integration/slack/create.go b/pkg/commands/integration/slack/create.go new file mode 100644 index 000000000..1ee225226 --- /dev/null +++ b/pkg/commands/integration/slack/create.go @@ -0,0 +1,75 @@ +package slack + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// CreateCommand calls the Fastly API to create a Slack notification integration. +type CreateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + IntegrationName string + Webhook string + + // Optional. + Description argparser.OptionalString +} + +// NewCreateCommand returns a usable command registered under the parent. +func NewCreateCommand(parent argparser.Registerer, g *global.Data) *CreateCommand { + c := CreateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("create", "Create a Slack notification integration").Alias("add") + + // Required. + c.CmdClause.Flag("name", "The name of the integration").Short('n').Required().StringVar(&c.IntegrationName) + c.CmdClause.Flag("webhook", "The Slack incoming webhook URL").Required().StringVar(&c.Webhook) + + // Optional. + c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *CreateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + input := &fastly.CreateIntegrationInput{ + Name: &c.IntegrationName, + Type: fastly.ToPointer(CommandName), + Config: map[string]string{"webhook": c.Webhook}, + } + if c.Description.WasSet { + input.Description = &c.Description.Value + } + + o, err := c.Globals.APIClient.CreateIntegration(context.TODO(), input) + if err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if ok, err := c.WriteJSON(out, o); ok { + return err + } + + text.Success(out, "Created Slack integration '%s' (id: %s)", c.IntegrationName, fastly.ToValue(o.ID)) + return nil +} diff --git a/pkg/commands/integration/slack/doc.go b/pkg/commands/integration/slack/doc.go new file mode 100644 index 000000000..0af712fb6 --- /dev/null +++ b/pkg/commands/integration/slack/doc.go @@ -0,0 +1,2 @@ +// Package slack contains commands to manage Slack notification integrations. +package slack diff --git a/pkg/commands/integration/slack/root.go b/pkg/commands/integration/slack/root.go new file mode 100644 index 000000000..95221751e --- /dev/null +++ b/pkg/commands/integration/slack/root.go @@ -0,0 +1,31 @@ +package slack + +import ( + "io" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/global" +) + +// CommandName is the string to be used to invoke this command. +const CommandName = "slack" + +// 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 +} + +// 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, "Manage Slack notification integrations") + return &c +} + +// Exec implements the command interface. +func (c *RootCommand) Exec(_ io.Reader, _ io.Writer) error { + panic("unreachable") +} diff --git a/pkg/commands/integration/slack/slack_test.go b/pkg/commands/integration/slack/slack_test.go new file mode 100644 index 000000000..a81eb0f1a --- /dev/null +++ b/pkg/commands/integration/slack/slack_test.go @@ -0,0 +1,126 @@ +package slack_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/fastly/go-fastly/v17/fastly" + + root "github.com/fastly/cli/pkg/commands/integration" + sub "github.com/fastly/cli/pkg/commands/integration/slack" + fstfmt "github.com/fastly/cli/pkg/fmt" + "github.com/fastly/cli/pkg/mock" + "github.com/fastly/cli/pkg/testutil" +) + +func TestCreateCommand(t *testing.T) { + const ( + integrationName = "test123" + integrationID = "integration-id-123" + webhookURL = "https://hooks.slack.com/services/xyz" + ) + + scenarios := []testutil.CLIScenario{ + { + Args: fmt.Sprintf("--webhook %s", webhookURL), + WantError: "error parsing arguments: required flag --name not provided", + }, + { + Args: fmt.Sprintf("--name %s", integrationName), + WantError: "error parsing arguments: required flag --webhook not provided", + }, + { + Args: fmt.Sprintf("--name %s --webhook %s", integrationName, webhookURL), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, _ *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return nil, errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: fmt.Sprintf("--name %s --webhook %s", integrationName, webhookURL), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, i *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + if fastly.ToValue(i.Type) != sub.CommandName { + return nil, fmt.Errorf("unexpected type: %s", fastly.ToValue(i.Type)) + } + if i.Config["webhook"] != webhookURL { + return nil, fmt.Errorf("unexpected webhook: %s", i.Config["webhook"]) + } + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.Success("Created Slack integration '%s' (id: %s)", integrationName, integrationID), + }, + { + Args: fmt.Sprintf("--name %s --webhook %s --json", integrationName, webhookURL), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, _ *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.EncodeJSON(&fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "create"}, scenarios) +} + +func TestUpdateCommand(t *testing.T) { + const ( + integrationID = "integration-id-123" + webhookURL = "https://hooks.slack.com/services/xyz" + ) + + scenarios := []testutil.CLIScenario{ + { + Args: fmt.Sprintf("--webhook %s", webhookURL), + WantError: "error parsing arguments: required argument 'id' not provided", + }, + { + Args: integrationID, + WantError: "error parsing arguments: required flag --webhook not provided", + }, + { + Args: fmt.Sprintf("%s --webhook %s", integrationID, webhookURL), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { + return errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: fmt.Sprintf("%s --webhook %s", integrationID, webhookURL), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, i *fastly.UpdateIntegrationInput) error { + if i.ID != integrationID { + return fmt.Errorf("unexpected id: %s", i.ID) + } + if fastly.ToValue(i.Type) != sub.CommandName { + return fmt.Errorf("unexpected type: %s", fastly.ToValue(i.Type)) + } + if i.Config["webhook"] != webhookURL { + return fmt.Errorf("unexpected webhook: %s", i.Config["webhook"]) + } + return nil + }, + }, + WantOutput: fstfmt.Success("Updated Slack integration (id: %s)", integrationID), + }, + { + Args: fmt.Sprintf("%s --webhook %s --json", integrationID, webhookURL), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { + return nil + }, + }, + WantOutput: fstfmt.JSON(`{"id": %q, "updated": true}`, integrationID), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "update"}, scenarios) +} diff --git a/pkg/commands/integration/slack/update.go b/pkg/commands/integration/slack/update.go new file mode 100644 index 000000000..8abfeb8b9 --- /dev/null +++ b/pkg/commands/integration/slack/update.go @@ -0,0 +1,87 @@ +package slack + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// UpdateCommand calls the Fastly API to update a Slack notification integration. +type UpdateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + ID string + Webhook string + + // Optional. + IntegrationName argparser.OptionalString + Description argparser.OptionalString +} + +// NewUpdateCommand returns a usable command registered under the parent. +func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateCommand { + c := UpdateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("update", "Update a Slack notification integration") + + // Required. + c.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.ID) + c.CmdClause.Flag("webhook", "The Slack incoming webhook URL").Required().StringVar(&c.Webhook) + + // Optional. + c.CmdClause.Flag("name", "The name of the integration").Short('n').Action(c.IntegrationName.Set).StringVar(&c.IntegrationName.Value) + c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *UpdateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + input := &fastly.UpdateIntegrationInput{ + ID: c.ID, + Type: fastly.ToPointer(CommandName), + Config: map[string]string{"webhook": c.Webhook}, + } + if c.IntegrationName.WasSet { + input.Name = &c.IntegrationName.Value + } + if c.Description.WasSet { + input.Description = &c.Description.Value + } + + if err := c.Globals.APIClient.UpdateIntegration(context.TODO(), input); err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if c.JSONOutput.Enabled { + o := struct { + ID string `json:"id"` + Updated bool `json:"updated"` + }{ + c.ID, + true, + } + _, err := c.WriteJSON(out, o) + return err + } + + text.Success(out, "Updated Slack integration (id: %s)", c.ID) + return nil +} diff --git a/pkg/commands/integration/splunkoncall/splunkoncall_test.go b/pkg/commands/integration/splunkoncall/splunkoncall_test.go index 3241dedf7..18fc20e26 100644 --- a/pkg/commands/integration/splunkoncall/splunkoncall_test.go +++ b/pkg/commands/integration/splunkoncall/splunkoncall_test.go @@ -78,14 +78,14 @@ func TestUpdateCommand(t *testing.T) { scenarios := []testutil.CLIScenario{ { Args: fmt.Sprintf("--url %s", webhookURL), - WantError: "error parsing arguments: required flag --id not provided", + WantError: "error parsing arguments: required argument 'id' not provided", }, { - Args: fmt.Sprintf("--id %s", integrationID), + Args: integrationID, WantError: "error parsing arguments: required flag --url not provided", }, { - Args: fmt.Sprintf("--id %s --url %s", integrationID, webhookURL), + Args: fmt.Sprintf("%s --url %s", integrationID, webhookURL), API: &mock.API{ UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { return errors.New("invalid request") @@ -94,7 +94,7 @@ func TestUpdateCommand(t *testing.T) { WantError: "invalid request", }, { - Args: fmt.Sprintf("--id %s --url %s", integrationID, webhookURL), + Args: fmt.Sprintf("%s --url %s", integrationID, webhookURL), API: &mock.API{ UpdateIntegrationFn: func(_ context.Context, i *fastly.UpdateIntegrationInput) error { if i.ID != integrationID { @@ -112,7 +112,7 @@ func TestUpdateCommand(t *testing.T) { WantOutput: fstfmt.Success("Updated Splunk On-Call integration (id: %s)", integrationID), }, { - Args: fmt.Sprintf("--id %s --url %s --json", integrationID, webhookURL), + Args: fmt.Sprintf("%s --url %s --json", integrationID, webhookURL), API: &mock.API{ UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { return nil diff --git a/pkg/commands/integration/splunkoncall/update.go b/pkg/commands/integration/splunkoncall/update.go index 4b5602031..8f61fccef 100644 --- a/pkg/commands/integration/splunkoncall/update.go +++ b/pkg/commands/integration/splunkoncall/update.go @@ -36,7 +36,7 @@ func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateComman c.CmdClause = parent.Command("update", "Update a Splunk On-Call notification integration") // Required. - c.RegisterFlag(argparser.IntegrationIDFlag(&c.ID)) + c.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.ID) c.CmdClause.Flag("url", "The Splunk On-Call webhook URL").Required().StringVar(&c.URL) // Optional. diff --git a/pkg/commands/integration/webhook/create.go b/pkg/commands/integration/webhook/create.go new file mode 100644 index 000000000..dd30a4337 --- /dev/null +++ b/pkg/commands/integration/webhook/create.go @@ -0,0 +1,75 @@ +package webhook + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// CreateCommand calls the Fastly API to create a Webhook notification integration. +type CreateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + IntegrationName string + Webhook string + + // Optional. + Description argparser.OptionalString +} + +// NewCreateCommand returns a usable command registered under the parent. +func NewCreateCommand(parent argparser.Registerer, g *global.Data) *CreateCommand { + c := CreateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("create", "Create a Webhook notification integration").Alias("add") + + // Required. + c.CmdClause.Flag("name", "The name of the integration").Short('n').Required().StringVar(&c.IntegrationName) + c.CmdClause.Flag("webhook", "The webhook URL to notify").Required().StringVar(&c.Webhook) + + // Optional. + c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *CreateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + input := &fastly.CreateIntegrationInput{ + Name: &c.IntegrationName, + Type: fastly.ToPointer(CommandName), + Config: map[string]string{"webhook": c.Webhook}, + } + if c.Description.WasSet { + input.Description = &c.Description.Value + } + + o, err := c.Globals.APIClient.CreateIntegration(context.TODO(), input) + if err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if ok, err := c.WriteJSON(out, o); ok { + return err + } + + text.Success(out, "Created Webhook integration '%s' (id: %s)", c.IntegrationName, fastly.ToValue(o.ID)) + return nil +} diff --git a/pkg/commands/integration/webhook/doc.go b/pkg/commands/integration/webhook/doc.go new file mode 100644 index 000000000..cd6b64c51 --- /dev/null +++ b/pkg/commands/integration/webhook/doc.go @@ -0,0 +1,3 @@ +// Package webhook contains commands to manage generic Webhook notification +// integrations. +package webhook diff --git a/pkg/commands/integration/webhook/get-signing-key.go b/pkg/commands/integration/webhook/get-signing-key.go new file mode 100644 index 000000000..613214bc3 --- /dev/null +++ b/pkg/commands/integration/webhook/get-signing-key.go @@ -0,0 +1,63 @@ +package webhook + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// GetSigningKeyCommand calls the Fastly API to get the signing key for a +// Webhook notification integration. +type GetSigningKeyCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + IntegrationID string +} + +// NewGetSigningKeyCommand returns a usable command registered under the parent. +func NewGetSigningKeyCommand(parent argparser.Registerer, g *global.Data) *GetSigningKeyCommand { + c := GetSigningKeyCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("get-signing-key", "Get the signing key for a Webhook notification integration") + + // Required. + c.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.IntegrationID) + + // Optional. + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *GetSigningKeyCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + input := &fastly.GetWebhookSigningKeyInput{IntegrationID: c.IntegrationID} + + o, err := c.Globals.APIClient.GetWebhookSigningKey(context.TODO(), input) + if err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if ok, err := c.WriteJSON(out, o); ok { + return err + } + + text.Success(out, "Signing key: '%s'", fastly.ToValue(o.SigningKey)) + return nil +} diff --git a/pkg/commands/integration/webhook/root.go b/pkg/commands/integration/webhook/root.go new file mode 100644 index 000000000..bd44dc45c --- /dev/null +++ b/pkg/commands/integration/webhook/root.go @@ -0,0 +1,31 @@ +package webhook + +import ( + "io" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/global" +) + +// CommandName is the string to be used to invoke this command. +const CommandName = "webhook" + +// 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 +} + +// 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, "Manage generic Webhook notification integrations") + return &c +} + +// Exec implements the command interface. +func (c *RootCommand) Exec(_ io.Reader, _ io.Writer) error { + panic("unreachable") +} diff --git a/pkg/commands/integration/webhook/rotate-signing-key.go b/pkg/commands/integration/webhook/rotate-signing-key.go new file mode 100644 index 000000000..7656f25d6 --- /dev/null +++ b/pkg/commands/integration/webhook/rotate-signing-key.go @@ -0,0 +1,63 @@ +package webhook + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// RotateSigningKeyCommand calls the Fastly API to rotate the signing key for +// a Webhook notification integration. +type RotateSigningKeyCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + IntegrationID string +} + +// NewRotateSigningKeyCommand returns a usable command registered under the parent. +func NewRotateSigningKeyCommand(parent argparser.Registerer, g *global.Data) *RotateSigningKeyCommand { + c := RotateSigningKeyCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("rotate-signing-key", "Rotate the signing key for a Webhook notification integration") + + // Required. + c.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.IntegrationID) + + // Optional. + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *RotateSigningKeyCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + input := &fastly.RotateWebhookSigningKeyInput{IntegrationID: c.IntegrationID} + + o, err := c.Globals.APIClient.RotateWebhookSigningKey(context.TODO(), input) + if err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if ok, err := c.WriteJSON(out, o); ok { + return err + } + + text.Success(out, "Signing key: '%s'", fastly.ToValue(o.SigningKey)) + return nil +} diff --git a/pkg/commands/integration/webhook/update.go b/pkg/commands/integration/webhook/update.go new file mode 100644 index 000000000..ce3813e63 --- /dev/null +++ b/pkg/commands/integration/webhook/update.go @@ -0,0 +1,87 @@ +package webhook + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v17/fastly" + + "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" +) + +// UpdateCommand calls the Fastly API to update a Webhook notification integration. +type UpdateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + ID string + Webhook string + + // Optional. + IntegrationName argparser.OptionalString + Description argparser.OptionalString +} + +// NewUpdateCommand returns a usable command registered under the parent. +func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateCommand { + c := UpdateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("update", "Update a Webhook notification integration") + + // Required. + c.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.ID) + c.CmdClause.Flag("webhook", "The webhook URL to notify").Required().StringVar(&c.Webhook) + + // Optional. + c.CmdClause.Flag("name", "The name of the integration").Short('n').Action(c.IntegrationName.Set).StringVar(&c.IntegrationName.Value) + c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *UpdateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + input := &fastly.UpdateIntegrationInput{ + ID: c.ID, + Type: fastly.ToPointer(CommandName), + Config: map[string]string{"webhook": c.Webhook}, + } + if c.IntegrationName.WasSet { + input.Name = &c.IntegrationName.Value + } + if c.Description.WasSet { + input.Description = &c.Description.Value + } + + if err := c.Globals.APIClient.UpdateIntegration(context.TODO(), input); err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if c.JSONOutput.Enabled { + o := struct { + ID string `json:"id"` + Updated bool `json:"updated"` + }{ + c.ID, + true, + } + _, err := c.WriteJSON(out, o) + return err + } + + text.Success(out, "Updated Webhook integration (id: %s)", c.ID) + return nil +} diff --git a/pkg/commands/integration/webhook/webhook_test.go b/pkg/commands/integration/webhook/webhook_test.go new file mode 100644 index 000000000..6c5e58aa7 --- /dev/null +++ b/pkg/commands/integration/webhook/webhook_test.go @@ -0,0 +1,216 @@ +package webhook_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/fastly/go-fastly/v17/fastly" + + root "github.com/fastly/cli/pkg/commands/integration" + sub "github.com/fastly/cli/pkg/commands/integration/webhook" + fstfmt "github.com/fastly/cli/pkg/fmt" + "github.com/fastly/cli/pkg/mock" + "github.com/fastly/cli/pkg/testutil" +) + +func TestCreateCommand(t *testing.T) { + const ( + integrationName = "test123" + integrationID = "integration-id-123" + webhookURL = "https://example.com/webhook" + ) + + scenarios := []testutil.CLIScenario{ + { + Args: fmt.Sprintf("--webhook %s", webhookURL), + WantError: "error parsing arguments: required flag --name not provided", + }, + { + Args: fmt.Sprintf("--name %s", integrationName), + WantError: "error parsing arguments: required flag --webhook not provided", + }, + { + Args: fmt.Sprintf("--name %s --webhook %s", integrationName, webhookURL), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, _ *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return nil, errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: fmt.Sprintf("--name %s --webhook %s", integrationName, webhookURL), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, i *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + if fastly.ToValue(i.Type) != sub.CommandName { + return nil, fmt.Errorf("unexpected type: %s", fastly.ToValue(i.Type)) + } + if i.Config["webhook"] != webhookURL { + return nil, fmt.Errorf("unexpected webhook: %s", i.Config["webhook"]) + } + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.Success("Created Webhook integration '%s' (id: %s)", integrationName, integrationID), + }, + { + Args: fmt.Sprintf("--name %s --webhook %s --json", integrationName, webhookURL), + API: &mock.API{ + CreateIntegrationFn: func(_ context.Context, _ *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) { + return &fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}, nil + }, + }, + WantOutput: fstfmt.EncodeJSON(&fastly.CreateIntegrationResponse{ID: fastly.ToPointer(integrationID)}), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "create"}, scenarios) +} + +func TestUpdateCommand(t *testing.T) { + const ( + integrationID = "integration-id-123" + webhookURL = "https://example.com/webhook" + ) + + scenarios := []testutil.CLIScenario{ + { + Args: fmt.Sprintf("--webhook %s", webhookURL), + WantError: "error parsing arguments: required argument 'id' not provided", + }, + { + Args: integrationID, + WantError: "error parsing arguments: required flag --webhook not provided", + }, + { + Args: fmt.Sprintf("%s --webhook %s", integrationID, webhookURL), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { + return errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: fmt.Sprintf("%s --webhook %s", integrationID, webhookURL), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, i *fastly.UpdateIntegrationInput) error { + if i.ID != integrationID { + return fmt.Errorf("unexpected id: %s", i.ID) + } + if fastly.ToValue(i.Type) != sub.CommandName { + return fmt.Errorf("unexpected type: %s", fastly.ToValue(i.Type)) + } + if i.Config["webhook"] != webhookURL { + return fmt.Errorf("unexpected webhook: %s", i.Config["webhook"]) + } + return nil + }, + }, + WantOutput: fstfmt.Success("Updated Webhook integration (id: %s)", integrationID), + }, + { + Args: fmt.Sprintf("%s --webhook %s --json", integrationID, webhookURL), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, _ *fastly.UpdateIntegrationInput) error { + return nil + }, + }, + WantOutput: fstfmt.JSON(`{"id": %q, "updated": true}`, integrationID), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "update"}, scenarios) +} + +func TestGetSigningKeyCommand(t *testing.T) { + const ( + integrationID = "integration-id-123" + signingKey = "sk_abc123" + ) + + scenarios := []testutil.CLIScenario{ + { + WantError: "error parsing arguments: required argument 'id' not provided", + }, + { + Args: integrationID, + API: &mock.API{ + GetWebhookSigningKeyFn: func(_ context.Context, _ *fastly.GetWebhookSigningKeyInput) (*fastly.WebhookSigningKeyResponse, error) { + return nil, errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: integrationID, + API: &mock.API{ + GetWebhookSigningKeyFn: func(_ context.Context, i *fastly.GetWebhookSigningKeyInput) (*fastly.WebhookSigningKeyResponse, error) { + if i.IntegrationID != integrationID { + return nil, fmt.Errorf("unexpected id: %s", i.IntegrationID) + } + return &fastly.WebhookSigningKeyResponse{SigningKey: fastly.ToPointer(signingKey)}, nil + }, + }, + WantOutput: fstfmt.Success("Signing key: '%s'", signingKey), + }, + { + Args: fmt.Sprintf("%s --json", integrationID), + API: &mock.API{ + GetWebhookSigningKeyFn: func(_ context.Context, _ *fastly.GetWebhookSigningKeyInput) (*fastly.WebhookSigningKeyResponse, error) { + return &fastly.WebhookSigningKeyResponse{SigningKey: fastly.ToPointer(signingKey)}, nil + }, + }, + WantOutput: fstfmt.EncodeJSON(&fastly.WebhookSigningKeyResponse{SigningKey: fastly.ToPointer(signingKey)}), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "get-signing-key"}, scenarios) +} + +func TestRotateSigningKeyCommand(t *testing.T) { + const ( + integrationID = "integration-id-123" + signingKey = "sk_abc123" + ) + + scenarios := []testutil.CLIScenario{ + { + WantError: "error parsing arguments: required argument 'id' not provided", + }, + { + Args: integrationID, + API: &mock.API{ + RotateWebhookSigningKeyFn: func(_ context.Context, _ *fastly.RotateWebhookSigningKeyInput) (*fastly.WebhookSigningKeyResponse, error) { + return nil, errors.New("invalid request") + }, + }, + WantError: "invalid request", + }, + { + Args: integrationID, + API: &mock.API{ + RotateWebhookSigningKeyFn: func(_ context.Context, i *fastly.RotateWebhookSigningKeyInput) (*fastly.WebhookSigningKeyResponse, error) { + if i.IntegrationID != integrationID { + return nil, fmt.Errorf("unexpected id: %s", i.IntegrationID) + } + return &fastly.WebhookSigningKeyResponse{SigningKey: fastly.ToPointer(signingKey)}, nil + }, + }, + WantOutput: fstfmt.Success("Signing key: '%s'", signingKey), + }, + { + Args: fmt.Sprintf("%s --json", integrationID), + API: &mock.API{ + RotateWebhookSigningKeyFn: func(_ context.Context, _ *fastly.RotateWebhookSigningKeyInput) (*fastly.WebhookSigningKeyResponse, error) { + return &fastly.WebhookSigningKeyResponse{SigningKey: fastly.ToPointer(signingKey)}, nil + }, + }, + WantOutput: fstfmt.EncodeJSON(&fastly.WebhookSigningKeyResponse{SigningKey: fastly.ToPointer(signingKey)}), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "rotate-signing-key"}, scenarios) +} diff --git a/pkg/mock/api.go b/pkg/mock/api.go index 8b85800c0..023f5fa3f 100644 --- a/pkg/mock/api.go +++ b/pkg/mock/api.go @@ -347,12 +347,15 @@ type API struct { UpdateServiceAuthorizationFn func(context.Context, *fastly.UpdateServiceAuthorizationInput) (*fastly.ServiceAuthorization, error) DeleteServiceAuthorizationFn func(context.Context, *fastly.DeleteServiceAuthorizationInput) error - SearchIntegrationsFn func(context.Context, *fastly.SearchIntegrationsInput) (*fastly.SearchIntegrationsResponse, error) - CreateIntegrationFn func(context.Context, *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) - GetIntegrationFn func(context.Context, *fastly.GetIntegrationInput) (*fastly.Integration, error) - UpdateIntegrationFn func(context.Context, *fastly.UpdateIntegrationInput) error - DeleteIntegrationFn func(context.Context, *fastly.DeleteIntegrationInput) error - GetIntegrationTypesFn func(context.Context) (*[]fastly.IntegrationType, error) + SearchIntegrationsFn func(context.Context, *fastly.SearchIntegrationsInput) (*fastly.SearchIntegrationsResponse, error) + CreateIntegrationFn func(context.Context, *fastly.CreateIntegrationInput) (*fastly.CreateIntegrationResponse, error) + GetIntegrationFn func(context.Context, *fastly.GetIntegrationInput) (*fastly.Integration, error) + UpdateIntegrationFn func(context.Context, *fastly.UpdateIntegrationInput) error + DeleteIntegrationFn func(context.Context, *fastly.DeleteIntegrationInput) error + GetIntegrationTypesFn func(context.Context) (*[]fastly.IntegrationType, error) + GetWebhookSigningKeyFn func(context.Context, *fastly.GetWebhookSigningKeyInput) (*fastly.WebhookSigningKeyResponse, error) + RotateWebhookSigningKeyFn func(context.Context, *fastly.RotateWebhookSigningKeyInput) (*fastly.WebhookSigningKeyResponse, error) + CreateMailinglistConfirmationFn func(context.Context, *fastly.CreateMailinglistConfirmationInput) error CreateConfigStoreFn func(context.Context, *fastly.CreateConfigStoreInput) (*fastly.ConfigStore, error) DeleteConfigStoreFn func(context.Context, *fastly.DeleteConfigStoreInput) error @@ -1856,6 +1859,21 @@ func (m API) GetIntegrationTypes(ctx context.Context) (*[]fastly.IntegrationType return m.GetIntegrationTypesFn(ctx) } +// GetWebhookSigningKey implements Interface. +func (m API) GetWebhookSigningKey(ctx context.Context, i *fastly.GetWebhookSigningKeyInput) (*fastly.WebhookSigningKeyResponse, error) { + return m.GetWebhookSigningKeyFn(ctx, i) +} + +// RotateWebhookSigningKey implements Interface. +func (m API) RotateWebhookSigningKey(ctx context.Context, i *fastly.RotateWebhookSigningKeyInput) (*fastly.WebhookSigningKeyResponse, error) { + return m.RotateWebhookSigningKeyFn(ctx, i) +} + +// CreateMailinglistConfirmation implements Interface. +func (m API) CreateMailinglistConfirmation(ctx context.Context, i *fastly.CreateMailinglistConfirmationInput) error { + return m.CreateMailinglistConfirmationFn(ctx, i) +} + // CreateConfigStore implements Interface. func (m API) CreateConfigStore(ctx context.Context, i *fastly.CreateConfigStoreInput) (*fastly.ConfigStore, error) { return m.CreateConfigStoreFn(ctx, i) From 8bb8c669eb88cdb430200d1e27d671d009783234 Mon Sep 17 00:00:00 2001 From: Anthony Gomez Date: Fri, 31 Jul 2026 14:56:05 -0400 Subject: [PATCH 3/5] Update CHANGELOG.md Co-authored-by: Kevin P. Fleming --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da4e4d04c..9f3d33d8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ ### Enhancements: -- feat(integrations): add platform integrations command group ([#1868](https://github.com/fastly/cli/pull/1868)) +- feat(integrations): Add support for Notification Service Integrations ([#1868](https://github.com/fastly/cli/pull/1868)) ### Dependencies: - build(deps): `golang.org/x/crypto` from 0.53.0 to 0.54.0 ([#1847](https://github.com/fastly/cli/pull/1847)) From 4e0ab0d0241542fde6bdac01bfd8ecc9ad51de76 Mon Sep 17 00:00:00 2001 From: Anthony Gomez Date: Fri, 31 Jul 2026 15:05:42 -0400 Subject: [PATCH 4/5] standardize types list --- pkg/commands/integration/list.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pkg/commands/integration/list.go b/pkg/commands/integration/list.go index 458f3dae0..8889cfdaf 100644 --- a/pkg/commands/integration/list.go +++ b/pkg/commands/integration/list.go @@ -7,11 +7,16 @@ import ( "github.com/fastly/go-fastly/v17/fastly" "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/commands/integration/datadog" + "github.com/fastly/cli/pkg/commands/integration/jiraissue" + "github.com/fastly/cli/pkg/commands/integration/jsm" "github.com/fastly/cli/pkg/commands/integration/mail" "github.com/fastly/cli/pkg/commands/integration/msteams" "github.com/fastly/cli/pkg/commands/integration/newrelic" + "github.com/fastly/cli/pkg/commands/integration/opsgenie" "github.com/fastly/cli/pkg/commands/integration/pagerduty" "github.com/fastly/cli/pkg/commands/integration/slack" + "github.com/fastly/cli/pkg/commands/integration/splunkoncall" "github.com/fastly/cli/pkg/commands/integration/webhook" fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/global" @@ -22,16 +27,16 @@ import ( // sub-families, offered as shell-completion hints for --type. Other type // values (e.g. legacy integration types) are still accepted. var knownIntegrationTypes = []string{ - fastly.IntegrationTypeDatadog, - fastly.IntegrationTypeJiraIssue, - fastly.IntegrationTypeJSM, - fastly.IntegrationTypeOpsGenie, - fastly.IntegrationTypeSplunkOnCall, + datadog.CommandName, + jiraissue.CommandName, + jsm.CommandName, mail.CommandName, msteams.CommandName, newrelic.CommandName, + opsgenie.CommandName, pagerduty.CommandName, slack.CommandName, + splunkoncall.CommandName, webhook.CommandName, } From 8b7100ed102ebab5d99391213ffb04533110cfc7 Mon Sep 17 00:00:00 2001 From: Anthony Gomez Date: Mon, 3 Aug 2026 10:32:30 -0400 Subject: [PATCH 5/5] notification integration commands (datadog, jiraissue, jsm, opsgenie, splunkoncall) required re-supplying secrets like --api-key just to change the name or description. The Notifications API PATCH endpoint only updates fields that are given, so relax these flags to optional and only populate Config with keys the user actually set. --- .../integration/datadog/datadog_test.go | 12 ++++- pkg/commands/integration/datadog/update.go | 21 +++++--- .../integration/jiraissue/jiraissue_test.go | 12 +++++ pkg/commands/integration/jiraissue/update.go | 51 +++++++++++-------- pkg/commands/integration/jsm/jsm_test.go | 12 ++++- pkg/commands/integration/jsm/update.go | 19 ++++--- .../integration/opsgenie/opsgenie_test.go | 12 ++++- pkg/commands/integration/opsgenie/update.go | 19 ++++--- .../splunkoncall/splunkoncall_test.go | 12 ++++- .../integration/splunkoncall/update.go | 19 ++++--- 10 files changed, 132 insertions(+), 57 deletions(-) diff --git a/pkg/commands/integration/datadog/datadog_test.go b/pkg/commands/integration/datadog/datadog_test.go index 22127b42d..2a1e59c99 100644 --- a/pkg/commands/integration/datadog/datadog_test.go +++ b/pkg/commands/integration/datadog/datadog_test.go @@ -93,8 +93,16 @@ func TestUpdateCommand(t *testing.T) { WantError: "error parsing arguments: required argument 'id' not provided", }, { - Args: integrationID, - WantError: "error parsing arguments: required flag --api-key not provided", + Args: fmt.Sprintf("%s --name new-name", integrationID), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, i *fastly.UpdateIntegrationInput) error { + if i.Config != nil { + return fmt.Errorf("unexpected config: %+v", i.Config) + } + return nil + }, + }, + WantOutput: fstfmt.Success("Updated Datadog integration (id: %s)", integrationID), }, { Args: fmt.Sprintf("%s --api-key %s", integrationID, apiKey), diff --git a/pkg/commands/integration/datadog/update.go b/pkg/commands/integration/datadog/update.go index ff29ed8c6..968b335a2 100644 --- a/pkg/commands/integration/datadog/update.go +++ b/pkg/commands/integration/datadog/update.go @@ -18,10 +18,10 @@ type UpdateCommand struct { argparser.JSONOutput // Required. - ID string - APIKey string + ID string // Optional. + APIKey argparser.OptionalString IntegrationName argparser.OptionalString Description argparser.OptionalString Site argparser.OptionalString @@ -38,9 +38,9 @@ func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateComman // Required. c.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.ID) - c.CmdClause.Flag("api-key", "Datadog API key").Required().StringVar(&c.APIKey) // Optional. + c.CmdClause.Flag("api-key", "Datadog API key").Action(c.APIKey.Set).StringVar(&c.APIKey.Value) c.CmdClause.Flag("name", "The name of the integration").Short('n').Action(c.IntegrationName.Set).StringVar(&c.IntegrationName.Value) c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) c.CmdClause.Flag("site", "Datadog site, e.g. \"datadoghq.eu\" (defaults to the US site)").Action(c.Site.Set).StringVar(&c.Site.Value) @@ -55,15 +55,20 @@ func (c *UpdateCommand) Exec(_ io.Reader, out io.Writer) error { return fsterr.ErrInvalidVerboseJSONCombo } - config := fastly.DatadogConfig{APIKey: c.APIKey} + config := map[string]string{} + if c.APIKey.WasSet { + config["apikey"] = c.APIKey.Value + } if c.Site.WasSet { - config.Site = c.Site.Value + config["site"] = c.Site.Value } input := &fastly.UpdateIntegrationInput{ - ID: c.ID, - Type: fastly.ToPointer(fastly.IntegrationTypeDatadog), - Config: config.ToMap(), + ID: c.ID, + Type: fastly.ToPointer(fastly.IntegrationTypeDatadog), + } + if len(config) > 0 { + input.Config = config } if c.IntegrationName.WasSet { input.Name = &c.IntegrationName.Value diff --git a/pkg/commands/integration/jiraissue/jiraissue_test.go b/pkg/commands/integration/jiraissue/jiraissue_test.go index 5aa856aa8..90bee9da2 100644 --- a/pkg/commands/integration/jiraissue/jiraissue_test.go +++ b/pkg/commands/integration/jiraissue/jiraissue_test.go @@ -75,6 +75,18 @@ func TestUpdateCommand(t *testing.T) { Args: requiredFlags, WantError: "error parsing arguments: required argument 'id' not provided", }, + { + Args: fmt.Sprintf("%s --name new-name", integrationID), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, i *fastly.UpdateIntegrationInput) error { + if i.Config != nil { + return fmt.Errorf("unexpected config: %+v", i.Config) + } + return nil + }, + }, + WantOutput: fstfmt.Success("Updated Jira Issue integration (id: %s)", integrationID), + }, { Args: fmt.Sprintf("%s %s", integrationID, requiredFlags), API: &mock.API{ diff --git a/pkg/commands/integration/jiraissue/update.go b/pkg/commands/integration/jiraissue/update.go index bdb279290..25b71e96d 100644 --- a/pkg/commands/integration/jiraissue/update.go +++ b/pkg/commands/integration/jiraissue/update.go @@ -18,14 +18,14 @@ type UpdateCommand struct { argparser.JSONOutput // Required. - ID string - BaseURL string - Username string - APIToken string - ProjectKey string - IssueType string + ID string // Optional. + BaseURL argparser.OptionalString + Username argparser.OptionalString + APIToken argparser.OptionalString + ProjectKey argparser.OptionalString + IssueType argparser.OptionalString IntegrationName argparser.OptionalString Description argparser.OptionalString } @@ -41,13 +41,13 @@ func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateComman // Required. c.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.ID) - c.CmdClause.Flag("base-url", "The base URL of the Jira instance").Required().StringVar(&c.BaseURL) - c.CmdClause.Flag("username", "The Jira username (email address) used to authenticate").Required().StringVar(&c.Username) - c.CmdClause.Flag("api-token", "The Jira API token").Required().StringVar(&c.APIToken) - c.CmdClause.Flag("project-key", "The key of the Jira project where issues will be created").Required().StringVar(&c.ProjectKey) - c.CmdClause.Flag("issue-type", "The type of Jira issue to create").Required().StringVar(&c.IssueType) // Optional. + c.CmdClause.Flag("base-url", "The base URL of the Jira instance").Action(c.BaseURL.Set).StringVar(&c.BaseURL.Value) + c.CmdClause.Flag("username", "The Jira username (email address) used to authenticate").Action(c.Username.Set).StringVar(&c.Username.Value) + c.CmdClause.Flag("api-token", "The Jira API token").Action(c.APIToken.Set).StringVar(&c.APIToken.Value) + c.CmdClause.Flag("project-key", "The key of the Jira project where issues will be created").Action(c.ProjectKey.Set).StringVar(&c.ProjectKey.Value) + c.CmdClause.Flag("issue-type", "The type of Jira issue to create").Action(c.IssueType.Set).StringVar(&c.IssueType.Value) c.CmdClause.Flag("name", "The name of the integration").Short('n').Action(c.IntegrationName.Set).StringVar(&c.IntegrationName.Value) c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) c.RegisterFlagBool(c.JSONFlag()) @@ -61,18 +61,29 @@ func (c *UpdateCommand) Exec(_ io.Reader, out io.Writer) error { return fsterr.ErrInvalidVerboseJSONCombo } - config := fastly.JiraIssueConfig{ - BaseURL: c.BaseURL, - Username: c.Username, - Token: c.APIToken, - ProjectKey: c.ProjectKey, - IssueType: c.IssueType, + config := map[string]string{} + if c.BaseURL.WasSet { + config["baseurl"] = c.BaseURL.Value + } + if c.Username.WasSet { + config["username"] = c.Username.Value + } + if c.APIToken.WasSet { + config["token"] = c.APIToken.Value + } + if c.ProjectKey.WasSet { + config["projectkey"] = c.ProjectKey.Value + } + if c.IssueType.WasSet { + config["issuetype"] = c.IssueType.Value } input := &fastly.UpdateIntegrationInput{ - ID: c.ID, - Type: fastly.ToPointer(fastly.IntegrationTypeJiraIssue), - Config: config.ToMap(), + ID: c.ID, + Type: fastly.ToPointer(fastly.IntegrationTypeJiraIssue), + } + if len(config) > 0 { + input.Config = config } if c.IntegrationName.WasSet { input.Name = &c.IntegrationName.Value diff --git a/pkg/commands/integration/jsm/jsm_test.go b/pkg/commands/integration/jsm/jsm_test.go index 1ad7610bb..342527d1e 100644 --- a/pkg/commands/integration/jsm/jsm_test.go +++ b/pkg/commands/integration/jsm/jsm_test.go @@ -81,8 +81,16 @@ func TestUpdateCommand(t *testing.T) { WantError: "error parsing arguments: required argument 'id' not provided", }, { - Args: integrationID, - WantError: "error parsing arguments: required flag --api-key not provided", + Args: fmt.Sprintf("%s --name new-name", integrationID), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, i *fastly.UpdateIntegrationInput) error { + if i.Config != nil { + return fmt.Errorf("unexpected config: %+v", i.Config) + } + return nil + }, + }, + WantOutput: fstfmt.Success("Updated Jira Service Management integration (id: %s)", integrationID), }, { Args: fmt.Sprintf("%s --api-key %s", integrationID, apiKey), diff --git a/pkg/commands/integration/jsm/update.go b/pkg/commands/integration/jsm/update.go index 151f1e40d..d4a5ef014 100644 --- a/pkg/commands/integration/jsm/update.go +++ b/pkg/commands/integration/jsm/update.go @@ -18,10 +18,10 @@ type UpdateCommand struct { argparser.JSONOutput // Required. - ID string - APIKey string + ID string // Optional. + APIKey argparser.OptionalString IntegrationName argparser.OptionalString Description argparser.OptionalString } @@ -37,9 +37,9 @@ func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateComman // Required. c.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.ID) - c.CmdClause.Flag("api-key", "Jira Service Management API key").Required().StringVar(&c.APIKey) // Optional. + c.CmdClause.Flag("api-key", "Jira Service Management API key").Action(c.APIKey.Set).StringVar(&c.APIKey.Value) c.CmdClause.Flag("name", "The name of the integration").Short('n').Action(c.IntegrationName.Set).StringVar(&c.IntegrationName.Value) c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) c.RegisterFlagBool(c.JSONFlag()) @@ -53,12 +53,17 @@ func (c *UpdateCommand) Exec(_ io.Reader, out io.Writer) error { return fsterr.ErrInvalidVerboseJSONCombo } - config := fastly.JSMConfig{APIKey: c.APIKey} + config := map[string]string{} + if c.APIKey.WasSet { + config["apikey"] = c.APIKey.Value + } input := &fastly.UpdateIntegrationInput{ - ID: c.ID, - Type: fastly.ToPointer(fastly.IntegrationTypeJSM), - Config: config.ToMap(), + ID: c.ID, + Type: fastly.ToPointer(fastly.IntegrationTypeJSM), + } + if len(config) > 0 { + input.Config = config } if c.IntegrationName.WasSet { input.Name = &c.IntegrationName.Value diff --git a/pkg/commands/integration/opsgenie/opsgenie_test.go b/pkg/commands/integration/opsgenie/opsgenie_test.go index f3123b012..790edc435 100644 --- a/pkg/commands/integration/opsgenie/opsgenie_test.go +++ b/pkg/commands/integration/opsgenie/opsgenie_test.go @@ -81,8 +81,16 @@ func TestUpdateCommand(t *testing.T) { WantError: "error parsing arguments: required argument 'id' not provided", }, { - Args: integrationID, - WantError: "error parsing arguments: required flag --api-key not provided", + Args: fmt.Sprintf("%s --name new-name", integrationID), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, i *fastly.UpdateIntegrationInput) error { + if i.Config != nil { + return fmt.Errorf("unexpected config: %+v", i.Config) + } + return nil + }, + }, + WantOutput: fstfmt.Success("Updated OpsGenie integration (id: %s)", integrationID), }, { Args: fmt.Sprintf("%s --api-key %s", integrationID, apiKey), diff --git a/pkg/commands/integration/opsgenie/update.go b/pkg/commands/integration/opsgenie/update.go index 4d00c9a68..b60e0766b 100644 --- a/pkg/commands/integration/opsgenie/update.go +++ b/pkg/commands/integration/opsgenie/update.go @@ -18,10 +18,10 @@ type UpdateCommand struct { argparser.JSONOutput // Required. - ID string - APIKey string + ID string // Optional. + APIKey argparser.OptionalString IntegrationName argparser.OptionalString Description argparser.OptionalString } @@ -37,9 +37,9 @@ func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateComman // Required. c.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.ID) - c.CmdClause.Flag("api-key", "OpsGenie API key").Required().StringVar(&c.APIKey) // Optional. + c.CmdClause.Flag("api-key", "OpsGenie API key").Action(c.APIKey.Set).StringVar(&c.APIKey.Value) c.CmdClause.Flag("name", "The name of the integration").Short('n').Action(c.IntegrationName.Set).StringVar(&c.IntegrationName.Value) c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) c.RegisterFlagBool(c.JSONFlag()) @@ -53,12 +53,17 @@ func (c *UpdateCommand) Exec(_ io.Reader, out io.Writer) error { return fsterr.ErrInvalidVerboseJSONCombo } - config := fastly.OpsGenieConfig{APIKey: c.APIKey} + config := map[string]string{} + if c.APIKey.WasSet { + config["apikey"] = c.APIKey.Value + } input := &fastly.UpdateIntegrationInput{ - ID: c.ID, - Type: fastly.ToPointer(fastly.IntegrationTypeOpsGenie), - Config: config.ToMap(), + ID: c.ID, + Type: fastly.ToPointer(fastly.IntegrationTypeOpsGenie), + } + if len(config) > 0 { + input.Config = config } if c.IntegrationName.WasSet { input.Name = &c.IntegrationName.Value diff --git a/pkg/commands/integration/splunkoncall/splunkoncall_test.go b/pkg/commands/integration/splunkoncall/splunkoncall_test.go index 18fc20e26..2b08d8eb4 100644 --- a/pkg/commands/integration/splunkoncall/splunkoncall_test.go +++ b/pkg/commands/integration/splunkoncall/splunkoncall_test.go @@ -81,8 +81,16 @@ func TestUpdateCommand(t *testing.T) { WantError: "error parsing arguments: required argument 'id' not provided", }, { - Args: integrationID, - WantError: "error parsing arguments: required flag --url not provided", + Args: fmt.Sprintf("%s --name new-name", integrationID), + API: &mock.API{ + UpdateIntegrationFn: func(_ context.Context, i *fastly.UpdateIntegrationInput) error { + if i.Config != nil { + return fmt.Errorf("unexpected config: %+v", i.Config) + } + return nil + }, + }, + WantOutput: fstfmt.Success("Updated Splunk On-Call integration (id: %s)", integrationID), }, { Args: fmt.Sprintf("%s --url %s", integrationID, webhookURL), diff --git a/pkg/commands/integration/splunkoncall/update.go b/pkg/commands/integration/splunkoncall/update.go index 8f61fccef..5163bf36b 100644 --- a/pkg/commands/integration/splunkoncall/update.go +++ b/pkg/commands/integration/splunkoncall/update.go @@ -18,10 +18,10 @@ type UpdateCommand struct { argparser.JSONOutput // Required. - ID string - URL string + ID string // Optional. + URL argparser.OptionalString IntegrationName argparser.OptionalString Description argparser.OptionalString } @@ -37,9 +37,9 @@ func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateComman // Required. c.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.ID) - c.CmdClause.Flag("url", "The Splunk On-Call webhook URL").Required().StringVar(&c.URL) // Optional. + c.CmdClause.Flag("url", "The Splunk On-Call webhook URL").Action(c.URL.Set).StringVar(&c.URL.Value) c.CmdClause.Flag("name", "The name of the integration").Short('n').Action(c.IntegrationName.Set).StringVar(&c.IntegrationName.Value) c.CmdClause.Flag("description", "A description of the integration").Action(c.Description.Set).StringVar(&c.Description.Value) c.RegisterFlagBool(c.JSONFlag()) @@ -53,12 +53,17 @@ func (c *UpdateCommand) Exec(_ io.Reader, out io.Writer) error { return fsterr.ErrInvalidVerboseJSONCombo } - config := fastly.SplunkOnCallConfig{URL: c.URL} + config := map[string]string{} + if c.URL.WasSet { + config["url"] = c.URL.Value + } input := &fastly.UpdateIntegrationInput{ - ID: c.ID, - Type: fastly.ToPointer(fastly.IntegrationTypeSplunkOnCall), - Config: config.ToMap(), + ID: c.ID, + Type: fastly.ToPointer(fastly.IntegrationTypeSplunkOnCall), + } + if len(config) > 0 { + input.Config = config } if c.IntegrationName.WasSet { input.Name = &c.IntegrationName.Value