diff --git a/CHANGELOG.md b/CHANGELOG.md index 07e43b8f9..9f3d33d8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ ### Enhancements: +- 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)) - 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..af61d0e80 100644 --- a/pkg/api/interface.go +++ b/pkg/api/interface.go @@ -357,6 +357,16 @@ 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) + 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 GetConfigStore(context.Context, *fastly.GetConfigStoreInput) (*fastly.ConfigStore, error) diff --git a/pkg/app/metadata.json b/pkg/app/metadata.json index f7e365e42..a1cf18902 100644 --- a/pkg/app/metadata.json +++ b/pkg/app/metadata.json @@ -2858,6 +2858,175 @@ ] } }, + "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" + ] + } + }, + "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": { "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/commands/commands.go b/pkg/commands/commands.go index ceecb1136..748b08496 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -69,6 +69,18 @@ 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" + 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" @@ -308,6 +320,47 @@ 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) + 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) @@ -1200,6 +1253,47 @@ 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, + 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/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..2a1e59c99 --- /dev/null +++ b/pkg/commands/integration/datadog/datadog_test.go @@ -0,0 +1,146 @@ +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 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 Datadog integration (id: %s)", integrationID), + }, + { + Args: fmt.Sprintf("%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("%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("%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..968b335a2 --- /dev/null +++ b/pkg/commands/integration/datadog/update.go @@ -0,0 +1,99 @@ +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 + + // Optional. + APIKey argparser.OptionalString + 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.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.ID) + + // 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) + 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 := map[string]string{} + if c.APIKey.WasSet { + config["apikey"] = c.APIKey.Value + } + if c.Site.WasSet { + config["site"] = c.Site.Value + } + + input := &fastly.UpdateIntegrationInput{ + ID: c.ID, + Type: fastly.ToPointer(fastly.IntegrationTypeDatadog), + } + if len(config) > 0 { + input.Config = config + } + 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..90bee9da2 --- /dev/null +++ b/pkg/commands/integration/jiraissue/jiraissue_test.go @@ -0,0 +1,126 @@ +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 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{ + 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) != 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("%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..25b71e96d --- /dev/null +++ b/pkg/commands/integration/jiraissue/update.go @@ -0,0 +1,114 @@ +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 + + // Optional. + BaseURL argparser.OptionalString + Username argparser.OptionalString + APIToken argparser.OptionalString + ProjectKey argparser.OptionalString + IssueType argparser.OptionalString + 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.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.ID) + + // 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()) + + 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 := 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), + } + if len(config) > 0 { + input.Config = config + } + 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..342527d1e --- /dev/null +++ b/pkg/commands/integration/jsm/jsm_test.go @@ -0,0 +1,134 @@ +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 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 Service Management integration (id: %s)", integrationID), + }, + { + Args: fmt.Sprintf("%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("%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("%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..d4a5ef014 --- /dev/null +++ b/pkg/commands/integration/jsm/update.go @@ -0,0 +1,94 @@ +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 + + // Optional. + APIKey argparser.OptionalString + 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.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.ID) + + // 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()) + + 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 := map[string]string{} + if c.APIKey.WasSet { + config["apikey"] = c.APIKey.Value + } + + input := &fastly.UpdateIntegrationInput{ + ID: c.ID, + Type: fastly.ToPointer(fastly.IntegrationTypeJSM), + } + if len(config) > 0 { + input.Config = config + } + 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..8889cfdaf --- /dev/null +++ b/pkg/commands/integration/list.go @@ -0,0 +1,106 @@ +package integration + +import ( + "context" + "io" + + "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" + "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{ + datadog.CommandName, + jiraissue.CommandName, + jsm.CommandName, + mail.CommandName, + msteams.CommandName, + newrelic.CommandName, + opsgenie.CommandName, + pagerduty.CommandName, + slack.CommandName, + splunkoncall.CommandName, + webhook.CommandName, +} + +// 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/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/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..790edc435 --- /dev/null +++ b/pkg/commands/integration/opsgenie/opsgenie_test.go @@ -0,0 +1,134 @@ +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 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 OpsGenie integration (id: %s)", integrationID), + }, + { + Args: fmt.Sprintf("%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("%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("%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..b60e0766b --- /dev/null +++ b/pkg/commands/integration/opsgenie/update.go @@ -0,0 +1,94 @@ +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 + + // Optional. + APIKey argparser.OptionalString + 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.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.ID) + + // 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()) + + 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 := map[string]string{} + if c.APIKey.WasSet { + config["apikey"] = c.APIKey.Value + } + + input := &fastly.UpdateIntegrationInput{ + ID: c.ID, + Type: fastly.ToPointer(fastly.IntegrationTypeOpsGenie), + } + if len(config) > 0 { + input.Config = config + } + 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/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/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/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/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..2b08d8eb4 --- /dev/null +++ b/pkg/commands/integration/splunkoncall/splunkoncall_test.go @@ -0,0 +1,134 @@ +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 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 Splunk On-Call integration (id: %s)", integrationID), + }, + { + Args: fmt.Sprintf("%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("%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("%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..5163bf36b --- /dev/null +++ b/pkg/commands/integration/splunkoncall/update.go @@ -0,0 +1,94 @@ +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 + + // Optional. + URL argparser.OptionalString + 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.CmdClause.Arg("id", "Integration ID").Required().StringVar(&c.ID) + + // 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()) + + 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 := map[string]string{} + if c.URL.WasSet { + config["url"] = c.URL.Value + } + + input := &fastly.UpdateIntegrationInput{ + ID: c.ID, + Type: fastly.ToPointer(fastly.IntegrationTypeSplunkOnCall), + } + if len(config) > 0 { + input.Config = config + } + 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/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 468056473..023f5fa3f 100644 --- a/pkg/mock/api.go +++ b/pkg/mock/api.go @@ -347,6 +347,16 @@ 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) + 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 GetConfigStoreFn func(context.Context, *fastly.GetConfigStoreInput) (*fastly.ConfigStore, error) @@ -1819,6 +1829,51 @@ 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) +} + +// 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) 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() +}