Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Event Orchestration: add support for Dynamic Routing and Dynamic Escalation Policy Assignment #885

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ require (
github.com/hashicorp/terraform-plugin-mux v0.13.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.31.0
github.com/hashicorp/terraform-plugin-testing v1.6.0
github.com/heimweh/go-pagerduty v0.0.0-20240503143637-3459408ac715
github.com/heimweh/go-pagerduty v0.0.0-20240722154207-95f2261a009a
)

require (
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S
github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/heimweh/go-pagerduty v0.0.0-20240503143637-3459408ac715 h1:DbdS2LIPkhsqgRcQzOAux0RpTJSH8VYOrN4rZZgznak=
github.com/heimweh/go-pagerduty v0.0.0-20240503143637-3459408ac715/go.mod h1:r59w5iyN01Qvi734yA5hZldbSeJJmsJzee/1kQ/MK7s=
github.com/heimweh/go-pagerduty v0.0.0-20240722154207-95f2261a009a h1:s7Z3DLwXMmfoeLEpgF8tfIxXGs8o734zcEVICTvAkAQ=
github.com/heimweh/go-pagerduty v0.0.0-20240722154207-95f2261a009a/go.mod h1:r59w5iyN01Qvi734yA5hZldbSeJJmsJzee/1kQ/MK7s=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
Expand Down
4 changes: 4 additions & 0 deletions pagerduty/event_orchestration_path_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -427,3 +427,7 @@ func emptyOrchestrationPathStructBuilder(pathType string) *pagerduty.EventOrches

return commonEmptyOrchestrationPath()
}

func isNonEmptyList(arg interface{}) bool {
return !isNilFunc(arg) && len(arg.([]interface{})) > 0
}
34 changes: 34 additions & 0 deletions pagerduty/import_pagerduty_event_orchestration_path_router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/heimweh/go-pagerduty/pagerduty"
)

func TestAccPagerDutyEventOrchestrationPathRouter_import(t *testing.T) {
Expand Down Expand Up @@ -37,6 +38,39 @@ func TestAccPagerDutyEventOrchestrationPathRouter_import(t *testing.T) {
})
}

func TestAccPagerDutyEventOrchestrationPathRouterDynamicRouteTo_import(t *testing.T) {
team := fmt.Sprintf("tf-name-%s", acctest.RandString(5))
escalationPolicy := fmt.Sprintf("tf-%s", acctest.RandString(5))
service := fmt.Sprintf("tf-%s", acctest.RandString(5))
orchestration := fmt.Sprintf("tf-orchestration-%s", acctest.RandString(5))
dynamicRouteToByNameInput := &pagerduty.EventOrchestrationPathDynamicRouteTo{
LookupBy: "service_name",
Regex: ".*",
Source: "event.custom_details.pd_service_name",
}

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckPagerDutyEventOrchestrationRouterDestroy,
Steps: []resource.TestStep{
{
Config: testAccCheckPagerDutyEventOrchestrationRouterDynamicRouteToConfig(team, escalationPolicy, service, orchestration, dynamicRouteToByNameInput),
},
{
ResourceName: "pagerduty_event_orchestration_router.router",
ImportStateIdFunc: testAccCheckPagerDutyEventOrchestrationPathRouterID,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{
"set.0.rule.0.id",
"set.0.rule.1.id",
},
},
},
})
}

func testAccCheckPagerDutyEventOrchestrationPathRouterID(s *terraform.State) (string, error) {
return s.RootModule().Resources["pagerduty_event_orchestration.orch"].Primary.ID, nil
}
22 changes: 14 additions & 8 deletions pagerduty/resource_pagerduty_event_orchestration_path_global.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ var eventOrchestrationPathGlobalCatchAllActionsSchema = map[string]*schema.Schem
Schema: eventOrchestrationIncidentCustomFieldsObjectSchema,
},
},
"escalation_policy": {
Type: schema.TypeString,
Optional: true,
},
}

var eventOrchestrationPathGlobalRuleActionsSchema = buildEventOrchestrationPathGlobalRuleActionsSchema()
Expand Down Expand Up @@ -374,6 +378,7 @@ func expandGlobalPathActions(v interface{}) *pagerduty.EventOrchestrationPathRul
actions.Suppress = a["suppress"].(bool)
actions.Suspend = intTypeToIntPtr(a["suspend"].(int))
actions.Priority = a["priority"].(string)
actions.EscalationPolicy = stringTypeToStringPtr(a["escalation_policy"].(string))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annotation: The EscalationPolicy property is a string pointer (*string) and not a string because we need to be able to send null to the API in order to reset the action.

actions.Annotate = a["annotate"].(string)
actions.Severity = a["severity"].(string)
actions.EventAction = a["event_action"].(string)
Expand Down Expand Up @@ -439,14 +444,15 @@ func flattenGlobalPathActions(actions *pagerduty.EventOrchestrationPathRuleActio
var actionsMap []map[string]interface{}

flattenedAction := map[string]interface{}{
"drop_event": actions.DropEvent,
"route_to": actions.RouteTo,
"severity": actions.Severity,
"event_action": actions.EventAction,
"suppress": actions.Suppress,
"suspend": actions.Suspend,
"priority": actions.Priority,
"annotate": actions.Annotate,
"drop_event": actions.DropEvent,
"route_to": actions.RouteTo,
"severity": actions.Severity,
"event_action": actions.EventAction,
"suppress": actions.Suppress,
"suspend": actions.Suspend,
"priority": actions.Priority,
"annotate": actions.Annotate,
"escalation_policy": stringPtrToStringType(actions.EscalationPolicy),
}

if actions.Variables != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ func TestAccPagerDutyEventOrchestrationPathGlobal_Basic(t *testing.T) {
resource.TestCheckResourceAttrSet(res, "set.0.rule.1.id"),
resource.TestCheckResourceAttrSet(res, "set.1.rule.0.id"),
resource.TestCheckResourceAttrSet(res, "set.1.rule.1.id"),
resource.TestCheckResourceAttr(
res, "set.0.rule.0.actions.0.escalation_policy", "POLICY3",
),
}...,
)...,
),
Expand Down Expand Up @@ -481,6 +484,7 @@ func testAccCheckPagerDutyEventOrchestrationPathGlobalAllActionsConfig(t, ep, s,
actions {
route_to = "set-1"
priority = "P0IN2KQ"
escalation_policy = pagerduty_escalation_policy.foo.id
annotate = "Routed through an event orchestration"
severity = "critical"
event_action = "trigger"
Expand Down Expand Up @@ -544,6 +548,7 @@ func testAccCheckPagerDutyEventOrchestrationPathGlobalAllActionsConfig(t, ep, s,
actions {
drop_event = true
priority = "P0IN2KW"
escalation_policy = pagerduty_escalation_policy.foo.id
annotate = "Routed through an event orchestration - catch-all rule"
severity = "warning"
event_action = "trigger"
Expand Down Expand Up @@ -589,6 +594,7 @@ func testAccCheckPagerDutyEventOrchestrationPathGlobalAllActionsUpdateConfig(t,
actions {
route_to = "set-2"
priority = "P0IN2KR"
escalation_policy = "POLICY3"
annotate = "Routed through a service orchestration!"
severity = "warning"
event_action = "resolve"
Expand Down Expand Up @@ -629,6 +635,7 @@ func testAccCheckPagerDutyEventOrchestrationPathGlobalAllActionsUpdateConfig(t,
label = "set-2 rule 1"
actions {
suspend = 15
escalation_policy = pagerduty_escalation_policy.foo.id
}
}
rule {
Expand Down Expand Up @@ -666,6 +673,7 @@ func testAccCheckPagerDutyEventOrchestrationPathGlobalAllActionsUpdateConfig(t,
actions {
drop_event = false
priority = "P0IN2KX"
escalation_policy = "POLICY4"
annotate = "[UPD] Routed through an event orchestration - catch-all rule"
severity = "info"
event_action = "resolve"
Expand Down
108 changes: 105 additions & 3 deletions pagerduty/resource_pagerduty_event_orchestration_path_router.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"log"
"net/http"
"strings"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
Expand All @@ -22,6 +23,7 @@ func resourcePagerDutyEventOrchestrationPathRouter() *schema.Resource {
Importer: &schema.ResourceImporter{
StateContext: resourcePagerDutyEventOrchestrationPathRouterImport,
},
CustomizeDiff: checkDynamicRoutingRule,
Schema: map[string]*schema.Schema{
"event_orchestration": {
Type: schema.TypeString,
Expand Down Expand Up @@ -63,9 +65,29 @@ func resourcePagerDutyEventOrchestrationPathRouter() *schema.Resource {
MaxItems: 1, // there can only be one action for router
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"dynamic_route_to": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"lookup_by": {
Type: schema.TypeString,
Required: true,
},
"regex": {
Type: schema.TypeString,
Required: true,
},
"source": {
Type: schema.TypeString,
Required: true,
},
},
},
},
"route_to": {
Type: schema.TypeString,
Required: true,
Optional: true,
ValidateFunc: func(v interface{}, key string) (warns []string, errs []error) {
value := v.(string)
if value == "unrouted" {
Expand Down Expand Up @@ -113,6 +135,54 @@ func resourcePagerDutyEventOrchestrationPathRouter() *schema.Resource {
}
}

func checkDynamicRoutingRule(context context.Context, diff *schema.ResourceDiff, i interface{}) error {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annotation: This function performs the following checks and returns a plan error if any of the checks are violated:

  • A Router can have at most one Dynamic Routing rule
  • Dynamic Routing rule must be the first rule of the first ("start") set
  • A Dynamic Routing rule, if placed correctly as the first rule of the first set, cannot have conditions and the route_to action

rNum := diff.Get("set.0.rule.#").(int)
draIdxs := []int{}
errorMsgs := []string{}

for ri := 0; ri < rNum; ri++ {
dra := diff.Get(fmt.Sprintf("set.0.rule.%d.actions.0.dynamic_route_to", ri))
hasDra := isNonEmptyList(dra)
if !hasDra {
continue
}
draIdxs = append(draIdxs, ri)
}
// 1. Only the first rule of the first ("start") set can have the Dynamic Routing action:
if len(draIdxs) > 1 {
idxs := []string{}
for _, idx := range draIdxs {
idxs = append(idxs, fmt.Sprintf("%d", idx))
}
errorMsgs = append(errorMsgs, fmt.Sprintf("A Router can have at most one Dynamic Routing rule; Rules with the dynamic_route_to action found at indexes: %s", strings.Join(idxs, ", ")))
}
// 2. The Dynamic Routing action can only be used in the first rule of the first set:
if len(draIdxs) > 0 && draIdxs[0] != 0 {
errorMsgs = append(errorMsgs, fmt.Sprintf("The Dynamic Routing rule must be the first rule in a Router"))
}
// 3. If the Dynamic Routing rule is the first rule of the first set,
// validate its configuration. It cannot have any conditions or the `route_to` action:
if len(draIdxs) == 1 && draIdxs[0] == 0 {
condNum := diff.Get("set.0.rule.0.condition.#").(int)
// diff.NewValueKnown(str) will return false if the value is based on interpolation that was unavailable at diff time,
// which may be the case for the `route_to` action when it references a pagerduty_service resource.
// Source: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/helper/schema#ResourceDiff.NewValueKnown
routeToValueKnown := diff.NewValueKnown("set.0.rule.0.actions.0.route_to")
routeTo := diff.Get("set.0.rule.0.actions.0.route_to").(string)
if condNum > 0 {
errorMsgs = append(errorMsgs, fmt.Sprintf("Dynamic Routing rules cannot have conditions"))
}
if !routeToValueKnown || routeToValueKnown && routeTo != "" {
errorMsgs = append(errorMsgs, fmt.Sprintf("Dynamic Routing rules cannot have the `route_to` action"))
}
}

if len(errorMsgs) > 0 {
return fmt.Errorf("Invalid Dynamic Routing rule configuration:\n- %s", strings.Join(errorMsgs, "\n- "))
}
return nil
}

func resourcePagerDutyEventOrchestrationPathRouterRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
var diags diag.Diagnostics

Expand Down Expand Up @@ -294,7 +364,12 @@ func expandRouterActions(v interface{}) *pagerduty.EventOrchestrationPathRuleAct
actions := new(pagerduty.EventOrchestrationPathRuleActions)
for _, ai := range v.([]interface{}) {
am := ai.(map[string]interface{})
actions.RouteTo = am["route_to"].(string)
dra := am["dynamic_route_to"]
if isNonEmptyList(dra) {
actions.DynamicRouteTo = expandRouterDynamicRouteToAction(dra)
} else {
actions.RouteTo = am["route_to"].(string)
}
Comment on lines +367 to +372
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annotation: Because dynamic_route_to and route_to are mutually-exclusive, we only set the route_to action if dynamic_route_to is not set

}

return actions
Expand All @@ -311,6 +386,17 @@ func expandCatchAll(v interface{}) *pagerduty.EventOrchestrationPathCatchAll {
return catchAll
}

func expandRouterDynamicRouteToAction(v interface{}) *pagerduty.EventOrchestrationPathDynamicRouteTo {
dr := new(pagerduty.EventOrchestrationPathDynamicRouteTo)
for _, i := range v.([]interface{}) {
dra := i.(map[string]interface{})
dr.LookupBy = dra["lookup_by"].(string)
dr.Regex = dra["regex"].(string)
dr.Source = dra["source"].(string)
}
return dr
}

func flattenSets(orchPathSets []*pagerduty.EventOrchestrationPathSet) []interface{} {
var flattenedSets []interface{}
for _, set := range orchPathSets {
Expand Down Expand Up @@ -344,7 +430,11 @@ func flattenRouterActions(actions *pagerduty.EventOrchestrationPathRuleActions)
var actionsMap []map[string]interface{}

am := make(map[string]interface{})
am["route_to"] = actions.RouteTo
if actions.DynamicRouteTo != nil {
am["dynamic_route_to"] = flattenRouterDynamicRouteToAction(actions.DynamicRouteTo)
} else {
am["route_to"] = actions.RouteTo
}
actionsMap = append(actionsMap, am)
return actionsMap
}
Expand All @@ -360,6 +450,18 @@ func flattenCatchAll(catchAll *pagerduty.EventOrchestrationPathCatchAll) []map[s
return caMap
}

func flattenRouterDynamicRouteToAction(dra *pagerduty.EventOrchestrationPathDynamicRouteTo) []map[string]interface{} {
var dr []map[string]interface{}

dr = append(dr, map[string]interface{}{
"lookup_by": dra.LookupBy,
"regex": dra.Regex,
"source": dra.Source,
})

return dr
}

func resourcePagerDutyEventOrchestrationPathRouterImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
client, err := meta.(*Config).Client()
if err != nil {
Expand Down
Loading
Loading