Repository: PagerDuty/go-pagerduty
Pull request #304 (2021-03-04 20:42:56)

Add support for Service Event Rules
Adding support for Service Event Rules. API reference:
[List Service's Event Rules](https://developer.pagerduty.com/api-reference/reference/REST/openapiv3.json/paths/~1services~1%7Bid%7D~1rules/get)
[Get an Event Rule from a Service](https://developer.pagerduty.com/api-reference/reference/REST/openapiv3.json/paths/~1services~1%7Bid%7D~1rules~1%7Brule_id%7D/get)
[Delete an Event Rule from a Service](https://developer.pagerduty.com/api-reference/reference/REST/openapiv3.json/paths/~1services~1%7Bid%7D~1rules~1%7Brule_id%7D/delete)
[Create an Event Rule on a Service](https://developer.pagerduty.com/api-reference/reference/REST/openapiv3.json/paths/~1services~1%7Bid%7D~1rules/post)
[Update an Event Rule on a Service](https://developer.pagerduty.com/api-reference/reference/REST/openapiv3.json/paths/~1services~1%7Bid%7D~1rules~1%7Brule_id%7D/put)

Some other changes:
  - Added the "suspend" rule action to keep the rule payload up to date with the API.
  - Adding Service Rule List and Service Rule Show CLI commands
  - Stubbing other possible Service Rule CLI commands
  
Edit: Forgot to mention, but I got trouble making the CLI logic work without changing the references to our own fork, so I [commented the code](https://github.com/PagerDuty/go-pagerduty/pull/304/files#diff-5f2221bb9234f08bad0a1e3ea4b31592def8ce460045cf712cc4e02d22c4828eR47) for now. As soon as we merge this PR, we can have the implemented CLI commands working by uncommenting this code.


Patch:
diff --git a/command/addon_install.go b/command/addon_install.go
index 9cb1bc8..f7d3df9 100644
--- a/command/addon_install.go
+++ b/command/addon_install.go
@@ -3,11 +3,12 @@ package main
 import (
 	"encoding/json"
 	"fmt"
-	"github.com/PagerDuty/go-pagerduty"
-	log "github.com/sirupsen/logrus"
-	"github.com/mitchellh/cli"
 	"os"
 	"strings"
+
+	"github.com/mitchellh/cli"
+	"github.com/PagerDuty/go-pagerduty"
+	log "github.com/sirupsen/logrus"
 )
 
 type AddonInstall struct {
diff --git a/command/main.go b/command/main.go
index 78965fe..376e693 100644
--- a/command/main.go
+++ b/command/main.go
@@ -2,8 +2,9 @@ package main
 
 import (
 	"fmt"
-	"github.com/mitchellh/cli"
 	"os"
+
+	"github.com/mitchellh/cli"
 )
 
 const (
@@ -69,6 +70,11 @@ func loadCommands() map[string]cli.CommandFactory {
 		"service integration create": ServiceIntegrationCreateCommand,
 		"service integration show":   ServiceIntegrationShowCommand,
 		"service integration update": ServiceIntegrationUpdateCommand,
+		"service rule create":        ServiceRuleCreateCommand,
+		"service rule delete":        ServiceRuleDeleteCommand,
+		"service rule list":          ServiceRuleListCommand,
+		"service rule show":          ServiceRuleShowCommand,
+		"service rule update":        ServiceRuleUpdateCommand,
 
 		"team list":                     TeamListCommand,
 		"team create":                   TeamShowCommand,
diff --git a/command/meta.go b/command/meta.go
index 2805144..4842c00 100644
--- a/command/meta.go
+++ b/command/meta.go
@@ -3,14 +3,15 @@ package main
 import (
 	"flag"
 	"fmt"
-	"github.com/PagerDuty/go-pagerduty"
-	log "github.com/sirupsen/logrus"
-	"github.com/mitchellh/go-homedir"
-	"gopkg.in/yaml.v2"
 	"io/ioutil"
 	"os"
 	"path/filepath"
 	"strings"
+
+	"github.com/mitchellh/go-homedir"
+	"github.com/PagerDuty/go-pagerduty"
+	log "github.com/sirupsen/logrus"
+	"gopkg.in/yaml.v2"
 )
 
 type ArrayFlags []string
diff --git a/command/service_integration_create.go b/command/service_integration_create.go
index d8f4e1c..b64253e 100644
--- a/command/service_integration_create.go
+++ b/command/service_integration_create.go
@@ -3,11 +3,12 @@ package main
 import (
 	"encoding/json"
 	"fmt"
-	"github.com/PagerDuty/go-pagerduty"
-	log "github.com/sirupsen/logrus"
-	"github.com/mitchellh/cli"
 	"os"
 	"strings"
+
+	"github.com/mitchellh/cli"
+	"github.com/PagerDuty/go-pagerduty"
+	log "github.com/sirupsen/logrus"
 )
 
 type ServiceIntegrationCreate struct {
diff --git a/command/service_list.go b/command/service_list.go
index 1be0240..08b36ff 100644
--- a/command/service_list.go
+++ b/command/service_list.go
@@ -2,11 +2,12 @@ package main
 
 import (
 	"fmt"
+	"strings"
+
 	"github.com/PagerDuty/go-pagerduty"
-	log "github.com/sirupsen/logrus"
 	"github.com/mitchellh/cli"
+	log "github.com/sirupsen/logrus"
 	"gopkg.in/yaml.v2"
-	"strings"
 )
 
 type ServiceList struct {
diff --git a/command/service_rule_create.go b/command/service_rule_create.go
new file mode 100644
index 0000000..93e4cc2
--- /dev/null
+++ b/command/service_rule_create.go
@@ -0,0 +1,28 @@
+package main
+
+import (
+	"strings"
+
+	"github.com/mitchellh/cli"
+)
+
+type ServiceRuleCreate struct {
+}
+
+func ServiceRuleCreateCommand() (cli.Command, error) {
+	return &ServiceRuleCreate{}, nil
+}
+
+func (c *ServiceRuleCreate) Help() string {
+	helpText := `
+	`
+	return strings.TrimSpace(helpText)
+}
+
+func (c *ServiceRuleCreate) Synopsis() string {
+	return "Get details about an integration belonging to a service"
+}
+
+func (c *ServiceRuleCreate) Run(args []string) int {
+	return 0
+}
diff --git a/command/service_rule_delete.go b/command/service_rule_delete.go
new file mode 100644
index 0000000..d7cedd0
--- /dev/null
+++ b/command/service_rule_delete.go
@@ -0,0 +1,28 @@
+package main
+
+import (
+	"strings"
+
+	"github.com/mitchellh/cli"
+)
+
+type ServiceRuleDelete struct {
+}
+
+func ServiceRuleDeleteCommand() (cli.Command, error) {
+	return &ServiceRuleDelete{}, nil
+}
+
+func (c *ServiceRuleDelete) Help() string {
+	helpText := `
+	`
+	return strings.TrimSpace(helpText)
+}
+
+func (c *ServiceRuleDelete) Synopsis() string {
+	return "Get details about an integration belonging to a service"
+}
+
+func (c *ServiceRuleDelete) Run(args []string) int {
+	return 0
+}
diff --git a/command/service_rule_list.go b/command/service_rule_list.go
new file mode 100644
index 0000000..e65a73c
--- /dev/null
+++ b/command/service_rule_list.go
@@ -0,0 +1,65 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/mitchellh/cli"
+	log "github.com/sirupsen/logrus"
+	"gopkg.in/yaml.v2"
+)
+
+type ServiceRuleList struct {
+	Meta
+}
+
+func ServiceRuleListCommand() (cli.Command, error) {
+	return &ServiceRuleList{}, nil
+}
+
+func (c *ServiceRuleList) Help() string {
+	helpText := `
+  pd service rules list <SERVICE_ID> List rules for service
+	` + c.Meta.Help()
+	return strings.TrimSpace(helpText)
+}
+
+func (c *ServiceRuleList) Synopsis() string {
+	return "List existing Rules for a service"
+}
+
+func (c *ServiceRuleList) Run(args []string) int {
+	flags := c.Meta.FlagSet("service rules list")
+	flags.Usage = func() { fmt.Println(c.Help()) }
+	if err := flags.Parse(args); err != nil {
+		log.Error(err)
+		return -1
+	}
+	if err := c.Meta.Setup(); err != nil {
+		log.Error(err)
+		return -1
+	}
+	if len(flags.Args()) != 1 {
+		log.Error("Please specify service id")
+		return -1
+	}
+	log.Info("Service id is:", flags.Arg(0))
+
+	client := c.Meta.Client()
+	if rulesList, err := client.ListServiceRulesPaginated(context.Background(), flags.Arg(0)); err != nil {
+		log.Error(err)
+		return -1
+	} else {
+		for i, rule := range rulesList {
+			fmt.Println("Entry: ", i+1)
+			data, err := yaml.Marshal(rule)
+			if err != nil {
+				log.Error(err)
+				return -1
+			}
+			fmt.Println(string(data))
+		}
+	}
+	return 0
+}
diff --git a/command/service_rule_show.go b/command/service_rule_show.go
new file mode 100644
index 0000000..a4dc673
--- /dev/null
+++ b/command/service_rule_show.go
@@ -0,0 +1,63 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/mitchellh/cli"
+	log "github.com/sirupsen/logrus"
+	"gopkg.in/yaml.v2"
+)
+
+type ServiceRuleShow struct {
+	Meta
+}
+
+func ServiceRuleShowCommand() (cli.Command, error) {
+	return &ServiceRuleShow{}, nil
+}
+
+func (c *ServiceRuleShow) Help() string {
+	helpText := `
+  pd service rules show <SERVICE_ID> <RULE_ID> Show specific service
+	` + c.Meta.Help()
+	return strings.TrimSpace(helpText)
+}
+
+func (c *ServiceRuleShow) Synopsis() string {
+	return "Get details about an existing service rule"
+}
+
+func (c *ServiceRuleShow) Run(args []string) int {
+	flags := c.Meta.FlagSet("service rules show")
+	flags.Usage = func() { fmt.Println(c.Help()) }
+	if err := flags.Parse(args); err != nil {
+		log.Error(err)
+		return -1
+	}
+	if err := c.Meta.Setup(); err != nil {
+		log.Error(err)
+		return -1
+	}
+	if len(flags.Args()) != 2 {
+		log.Error("Please specify service id and rule id")
+		return -1
+	}
+	log.Info("Service id is:", flags.Arg(0))
+	log.Info("Rule id is:", flags.Arg(1))
+
+	client := c.Meta.Client()
+	rule, err := client.GetServiceRule(context.Background(), flags.Arg(0), flags.Arg(1))
+	if err != nil {
+		log.Error(err)
+		return -1
+	}
+	data, err := yaml.Marshal(rule)
+	if err != nil {
+		log.Error(err)
+		return -1
+	}
+	fmt.Println(string(data))
+	return 0
+}
diff --git a/command/service_rule_update.go b/command/service_rule_update.go
new file mode 100644
index 0000000..117d50c
--- /dev/null
+++ b/command/service_rule_update.go
@@ -0,0 +1,28 @@
+package main
+
+import (
+	"strings"
+
+	"github.com/mitchellh/cli"
+)
+
+type ServiceRuleUpdate struct {
+}
+
+func ServiceRuleUpdateCommand() (cli.Command, error) {
+	return &ServiceRuleUpdate{}, nil
+}
+
+func (c *ServiceRuleUpdate) Help() string {
+	helpText := `
+	`
+	return strings.TrimSpace(helpText)
+}
+
+func (c *ServiceRuleUpdate) Synopsis() string {
+	return "Get details about an integration belonging to a service"
+}
+
+func (c *ServiceRuleUpdate) Run(args []string) int {
+	return 0
+}
diff --git a/event.go b/event.go
index 6216899..e162fd5 100644
--- a/event.go
+++ b/event.go
@@ -49,7 +49,7 @@ func CreateEventWithHTTPClient(e Event, client HTTPClient) (*EventResponse, erro
 	req.Header.Set("Content-Type", "application/json")
 	resp, err := client.Do(req)
 	if err != nil {
-		return nil, fmt.Errorf("failed to action request: %w", err)
+		return &EventResponse{HttpStatus: resp.StatusCode}, err
 	}
 	defer resp.Body.Close()
 	if resp.StatusCode != http.StatusOK {
diff --git a/ruleset.go b/ruleset.go
index d1f96f6..acce74e 100644
--- a/ruleset.go
+++ b/ruleset.go
@@ -104,13 +104,14 @@ type ListRulesetRulesResponse struct {
 
 // RuleActions represents a rule action
 type RuleActions struct {
-	Suppress    *RuleActionSuppress     `json:"suppress,omitempty"`
 	Annotate    *RuleActionParameter    `json:"annotate,omitempty"`
-	Severity    *RuleActionParameter    `json:"severity,omitempty"`
-	Priority    *RuleActionParameter    `json:"priority,omitempty"`
-	Route       *RuleActionParameter    `json:"route"`
 	EventAction *RuleActionParameter    `json:"event_action,omitempty"`
 	Extractions []*RuleActionExtraction `json:"extractions,omitempty"`
+	Priority    *RuleActionParameter    `json:"priority,omitempty"`
+	Severity    *RuleActionParameter    `json:"severity,omitempty"`
+	Suppress    *RuleActionSuppress     `json:"suppress,omitempty"`
+	Suspend     *RuleActionSuspend      `json:"suspend,omitempty"`
+	Route       *RuleActionParameter    `json:"route"`
 }
 
 // RuleActionParameter represents a generic parameter object on a rule action
@@ -126,6 +127,11 @@ type RuleActionSuppress struct {
 	ThresholdTimeAmount int    `json:"threshold_time_amount,omitempty"`
 }
 
+// RuleActionSuspend represents a rule suspend action object
+type RuleActionSuspend struct {
+	Value *bool `json:"value,omitempty"`
+}
+
 // RuleActionExtraction represents a rule extraction action object
 type RuleActionExtraction struct {
 	Target string `json:"target,omitempty"`
diff --git a/service.go b/service.go
index 8f6f838..02074f2 100644
--- a/service.go
+++ b/service.go
@@ -56,6 +56,37 @@ type IncidentUrgencyRule struct {
 	OutsideSupportHours *IncidentUrgencyType `json:"outside_support_hours,omitempty"`
 }
 
+// ListServiceRulesResponse represents a list of rules in a service
+type ListServiceRulesResponse struct {
+	Offset uint          `json:"offset,omitempty"`
+	Limit  uint          `json:"limit,omitempty"`
+	More   bool          `json:"more,omitempty"`
+	Total  uint          `json:"total,omitempty"`
+	Rules  []ServiceRule `json:"rules,omitempty"`
+}
+
+// ServiceRule represents a Service rule
+type ServiceRule struct {
+	ID         string              `json:"id,omitempty"`
+	Self       string              `json:"self,omitempty"`
+	Disabled   *bool               `json:"disabled,omitempty"`
+	Conditions *RuleConditions     `json:"conditions,omitempty"`
+	TimeFrame  *RuleTimeFrame      `json:"time_frame,omitempty"`
+	Position   *int                `json:"position,omitempty"`
+	Actions    *ServiceRuleActions `json:"actions,omitempty"`
+}
+
+// ServiceRuleActions represents a rule action
+type ServiceRuleActions struct {
+	Annotate    *RuleActionParameter   `json:"annotate,omitempty"`
+	EventAction *RuleActionParameter   `json:"event_action,omitempty"`
+	Extractions []RuleActionExtraction `json:"extractions,omitempty"`
+	Priority    *RuleActionParameter   `json:"priority,omitempty"`
+	Severity    *RuleActionParameter   `json:"severity,omitempty"`
+	Suppress    *RuleActionSuppress    `json:"suppress,omitempty"`
+	Suspend     *RuleActionSuspend     `json:"suspend,omitempty"`
+}
+
 // Service represents something you monitor (like a web service, email service, or database service).
 type Service struct {
 	APIObject
@@ -220,6 +251,88 @@ func (c *Client) DeleteIntegration(serviceID string, integrationID string) error
 	return err
 }
 
+// ListServiceRules gets all rules for a service.
+func (c *Client) ListServiceRulesPaginated(ctx context.Context, serviceID string) ([]ServiceRule, error) {
+	var rules []ServiceRule
+
+	// Create a handler closure capable of parsing data from the Service rules endpoint
+	// and appending resultant Service rules to the return slice.
+	responseHandler := func(response *http.Response) (APIListObject, error) {
+		var result ListServiceRulesResponse
+
+		if err := c.decodeJSON(response, &result); err != nil {
+			return APIListObject{}, err
+		}
+
+		rules = append(rules, result.Rules...)
+
+		// Return stats on the current page. Caller can use this information to
+		// adjust for requesting additional pages.
+		return APIListObject{
+			More:   result.More,
+			Offset: result.Offset,
+			Limit:  result.Limit,
+		}, nil
+	}
+
+	// Make call to get all pages associated with the base endpoint.
+	if err := c.pagedGet(ctx, "/services/"+serviceID+"/rules", responseHandler); err != nil {
+		return nil, err
+	}
+
+	return rules, nil
+}
+
+// GetServiceRule gets a service rule.
+func (c *Client) GetServiceRule(ctx context.Context, serviceID, ruleID string) (ServiceRule, error) {
+	resp, err := c.get(ctx, "/services/"+serviceID+"/rules/"+ruleID)
+	return getServiceRuleFromResponse(c, resp, err)
+}
+
+// DeleteServiceRule deletes a service rule.
+func (c *Client) DeleteServiceRule(ctx context.Context, serviceID, ruleID string) error {
+	_, err := c.delete(ctx, "/services/"+serviceID+"/rules/"+ruleID)
+	return err
+}
+
+// CreateServiceRule creates a service rule.
+func (c *Client) CreateServiceRule(ctx context.Context, serviceID string, rule ServiceRule) (ServiceRule, error) {
+	d := map[string]ServiceRule{
+		"rule": rule,
+	}
+	resp, err := c.post(ctx, "/services/"+serviceID+"/rules/", d, nil)
+	return getServiceRuleFromResponse(c, resp, err)
+}
+
+// UpdateServiceRule updates a service rule.
+func (c *Client) UpdateServiceRule(ctx context.Context, serviceID, ruleID string, rule ServiceRule) (ServiceRule, error) {
+	d := map[string]ServiceRule{
+		"rule": rule,
+	}
+	resp, err := c.put(ctx, "/services/"+serviceID+"/rules/"+ruleID, d, nil)
+	return getServiceRuleFromResponse(c, resp, err)
+}
+
+func getServiceRuleFromResponse(c *Client, resp *http.Response, err error) (ServiceRule, error) {
+	if err != nil {
+		return ServiceRule{}, err
+	}
+
+	var target map[string]ServiceRule
+	if dErr := c.decodeJSON(resp, &target); dErr != nil {
+		return ServiceRule{}, fmt.Errorf("Could not decode JSON response: %v", dErr)
+	}
+
+	const rootNode = "rule"
+
+	t, nodeOK := target[rootNode]
+	if !nodeOK {
+		return ServiceRule{}, fmt.Errorf("JSON response does not have %s field", rootNode)
+	}
+
+	return t, nil
+}
+
 func getServiceFromResponse(c *Client, resp *http.Response, err error) (*Service, error) {
 	if err != nil {
 		return nil, err


Test patch:
diff --git a/service_test.go b/service_test.go
index 6a482e1..445bacb 100644
--- a/service_test.go
+++ b/service_test.go
@@ -429,3 +429,123 @@ func TestService_DeleteIntegration(t *testing.T) {
 		t.Fatal(err)
 	}
 }
+
+// List Service Rules
+func TestService_ListRules(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/services/1/rules", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		w.Write([]byte(`{"rules": [{"id": "1"}]}`))
+	})
+
+	var client = &Client{apiEndpoint: server.URL, authToken: "foo", HTTPClient: defaultHTTPClient}
+
+	serviceID := "1"
+	res, err := client.ListServiceRulesPaginated(context.Background(), serviceID)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want := []ServiceRule{{ID: "1"}}
+	testEqual(t, want, res)
+}
+
+// Create Service Rule
+func TestService_CreateServiceRule(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/services/1/rules/", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "POST")
+		w.Write([]byte(`{"rule": {"id": "1"}}`))
+	})
+
+	var client = &Client{apiEndpoint: server.URL, authToken: "foo", HTTPClient: defaultHTTPClient}
+
+	serviceID := "1"
+	rule := ServiceRule{}
+
+	res, err := client.CreateServiceRule(context.Background(), serviceID, rule)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want := ServiceRule{
+		ID: "1",
+	}
+	testEqual(t, want, res)
+}
+
+// Get Service Rule
+func TestService_GetServiceRule(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/services/1/rules/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		w.Write([]byte(`{"rule": {"id": "1"}}`))
+	})
+
+	var client = &Client{apiEndpoint: server.URL, authToken: "foo", HTTPClient: defaultHTTPClient}
+
+	serviceID := "1"
+	ruleID := "1"
+	res, err := client.GetServiceRule(context.Background(), serviceID, ruleID)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want := ServiceRule{
+		ID: "1",
+	}
+	testEqual(t, want, res)
+}
+
+// Update Service Rule
+func TestService_UpdateServiceRule(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/services/1/rules/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "PUT")
+		w.Write([]byte(`{"rule": {"id": "1"}}`))
+	})
+
+	var client = &Client{apiEndpoint: server.URL, authToken: "foo", HTTPClient: defaultHTTPClient}
+
+	serviceID := "1"
+	ruleID := "1"
+	rule := ServiceRule{}
+
+	res, err := client.UpdateServiceRule(context.Background(), serviceID, ruleID, rule)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want := ServiceRule{
+		ID: "1",
+	}
+	testEqual(t, want, res)
+}
+
+// Delete Service Rule
+func TestService_DeleteServiceRule(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/services/1/rules/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "DELETE")
+	})
+
+	var client = &Client{apiEndpoint: server.URL, authToken: "foo", HTTPClient: defaultHTTPClient}
+	serviceID := "1"
+	ruleID := "1"
+
+	err := client.DeleteServiceRule(context.Background(), serviceID, ruleID)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+}