From 10b848f868eaeeb029df2ac01835aa7d3e12e030 Mon Sep 17 00:00:00 2001 From: alenapan <47909261+alenapan@users.noreply.github.com> Date: Thu, 20 Jun 2024 12:30:49 -0700 Subject: [PATCH] ORCA-4813 - Add validation for Dynamic Routing rule (#2) --- pagerduty/event_orchestration_path_util.go | 4 + ...gerduty_event_orchestration_path_router.go | 52 ++++++- ...ty_event_orchestration_path_router_test.go | 133 +++++++++++++++++- 3 files changed, 186 insertions(+), 3 deletions(-) diff --git a/pagerduty/event_orchestration_path_util.go b/pagerduty/event_orchestration_path_util.go index 92f7b41de..f69d12656 100644 --- a/pagerduty/event_orchestration_path_util.go +++ b/pagerduty/event_orchestration_path_util.go @@ -427,3 +427,7 @@ func emptyOrchestrationPathStructBuilder(pathType string) *pagerduty.EventOrches return commonEmptyOrchestrationPath() } + +func isNonEmptyList(arg interface{}) bool { + return !isNilFunc(arg) && len(arg.([]interface{})) > 0 +} diff --git a/pagerduty/resource_pagerduty_event_orchestration_path_router.go b/pagerduty/resource_pagerduty_event_orchestration_path_router.go index 873864b42..9be29d6f0 100644 --- a/pagerduty/resource_pagerduty_event_orchestration_path_router.go +++ b/pagerduty/resource_pagerduty_event_orchestration_path_router.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "net/http" + "strings" "time" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -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, @@ -133,6 +135,54 @@ func resourcePagerDutyEventOrchestrationPathRouter() *schema.Resource { } } +func checkDynamicRoutingRule(context context.Context, diff *schema.ResourceDiff, i interface{}) error { + 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 @@ -315,7 +365,7 @@ func expandRouterActions(v interface{}) *pagerduty.EventOrchestrationPathRuleAct for _, ai := range v.([]interface{}) { am := ai.(map[string]interface{}) dra := am["dynamic_route_to"] - if !isNilFunc(dra) && len(dra.([]interface{})) > 0 { + if isNonEmptyList(dra) { actions.DynamicRouteTo = expandRouterDynamicRouteToAction(dra) } else { actions.RouteTo = am["route_to"].(string) diff --git a/pagerduty/resource_pagerduty_event_orchestration_path_router_test.go b/pagerduty/resource_pagerduty_event_orchestration_path_router_test.go index c2885623e..fcf81f2c1 100644 --- a/pagerduty/resource_pagerduty_event_orchestration_path_router_test.go +++ b/pagerduty/resource_pagerduty_event_orchestration_path_router_test.go @@ -3,6 +3,7 @@ package pagerduty import ( "context" "fmt" + "regexp" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" @@ -34,12 +35,32 @@ func TestAccPagerDutyEventOrchestrationPathRouter_Basic(t *testing.T) { Regex: "ID:(.*)", Source: "event.custom_details.pd_service_id", } + invalidDynamicRouteToPlacementMessage := "Invalid Dynamic Routing rule configuration:\n- A Router can have at most one Dynamic Routing rule; Rules with the dynamic_route_to action found at indexes: 1, 2\n- The Dynamic Routing rule must be the first rule in a Router" + invalidDynamicRouteToConfigMessage := "Invalid Dynamic Routing rule configuration:\n- Dynamic Routing rules cannot have conditions\n- Dynamic Routing rules cannot have the `route_to` action" resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckPagerDutyEventOrchestrationRouterDestroy, Steps: []resource.TestStep{ + // Invalid Dynamic Routing rule config for a new resource: multiple Dynamic Routing rules, Dynamic Routing rule not the first rule in the Router: + { + Config: testAccCheckPagerDutyEventOrchestrationRouterInvalidDynamicRoutingRulePlacement(team, escalationPolicy, service, orchestration), + PlanOnly: true, + ExpectError: regexp.MustCompile(invalidDynamicRouteToPlacementMessage), + }, + // Invalid Dynamic Routing rule config for a new resource: Dynamic Routing rule with conditions and the interpolated route_to action: + { + Config: testAccCheckPagerDutyEventOrchestrationRouterInvalidDynamicRoutingRuleConfig(team, escalationPolicy, service, orchestration, "pagerduty_service.bar.id"), + PlanOnly: true, + ExpectError: regexp.MustCompile(invalidDynamicRouteToConfigMessage), + }, + // Invalid Dynamic Routing rule config for a new resource: Dynamic Routing rule with conditions and the hard-coded route_to action: + { + Config: testAccCheckPagerDutyEventOrchestrationRouterInvalidDynamicRoutingRuleConfig(team, escalationPolicy, service, orchestration, "\"PARASOL\""), + PlanOnly: true, + ExpectError: regexp.MustCompile(invalidDynamicRouteToConfigMessage), + }, { Config: testAccCheckPagerDutyEventOrchestrationRouterConfigNoRules(team, escalationPolicy, service, orchestration), Check: resource.ComposeTestCheckFunc( @@ -70,7 +91,25 @@ func TestAccPagerDutyEventOrchestrationPathRouter_Basic(t *testing.T) { "pagerduty_event_orchestration_router.router", "unrouted", true), ), }, - // Update the Dynamic Routing rule added in the previous test case: + // Invalid Dynamic Routing rule config for an existing resource: multiple Dynamic Routing rules, Dynamic Routing rule not the first rule in the Router: + { + Config: testAccCheckPagerDutyEventOrchestrationRouterInvalidDynamicRoutingRulePlacement(team, escalationPolicy, service, orchestration), + PlanOnly: true, + ExpectError: regexp.MustCompile(invalidDynamicRouteToPlacementMessage), + }, + // Invalid Dynamic Routing rule config for an existing resource: Dynamic Routing rule with conditions and the interpolated route_to action: + { + Config: testAccCheckPagerDutyEventOrchestrationRouterInvalidDynamicRoutingRuleConfig(team, escalationPolicy, service, orchestration, "pagerduty_service.bar.id"), + PlanOnly: true, + ExpectError: regexp.MustCompile(invalidDynamicRouteToConfigMessage), + }, + // Invalid Dynamic Routing rule config for an existing resource: Dynamic Routing rule with conditions and the hard-coded route_to action: + { + Config: testAccCheckPagerDutyEventOrchestrationRouterInvalidDynamicRoutingRuleConfig(team, escalationPolicy, service, orchestration, "\"PARASOL\""), + PlanOnly: true, + ExpectError: regexp.MustCompile(invalidDynamicRouteToConfigMessage), + }, + // Update the Dynamic Routing rule: { Config: testAccCheckPagerDutyEventOrchestrationRouterDynamicRouteToConfig(team, escalationPolicy, service, orchestration, dynamicRouteToByIDInput), Check: resource.ComposeTestCheckFunc( @@ -80,7 +119,7 @@ func TestAccPagerDutyEventOrchestrationPathRouter_Basic(t *testing.T) { "pagerduty_event_orchestration_router.router", "unrouted", true), ), }, - // Delete the Dynamic Routing rule added in the previous test cases: + // Delete the Dynamic Routing rule: { Config: testAccCheckPagerDutyEventOrchestrationRouterConfigWithConditions(team, escalationPolicy, service, orchestration), Check: resource.ComposeTestCheckFunc( @@ -271,6 +310,96 @@ func createBaseConfig(t, ep, s, o string) string { `, t, ep, s, o) } +func testAccCheckPagerDutyEventOrchestrationRouterInvalidDynamicRoutingRulePlacement(t, ep, s, o string) string { + return fmt.Sprintf("%s%s", createBaseConfig(t, ep, s, o), + `resource "pagerduty_event_orchestration_router" "router" { + event_orchestration = pagerduty_event_orchestration.orch.id + + catch_all { + actions { + route_to = "unrouted" + } + } + set { + id = "start" + rule { + label = "static routing rule 1" + actions { + route_to = pagerduty_service.bar.id + } + } + rule { + disabled = false + label = "dynamic routing rule 1" + actions { + dynamic_route_to { + lookup_by = "service_id" + regex = ".*" + source = "event.custom_details.pd_service_id" + } + } + } + rule { + label = "dynamic routing rule 2" + actions { + dynamic_route_to { + lookup_by = "service_name" + regex = ".*" + source = "event.custom_details.pd_service_name" + } + } + } + rule { + label = "static routing rule 2" + actions { + route_to = "P1B2C23" + } + } + + } + } + `) +} + +func testAccCheckPagerDutyEventOrchestrationRouterInvalidDynamicRoutingRuleConfig(t, ep, s, o, routeTo string) string { + routerConfig := fmt.Sprintf( + `resource "pagerduty_event_orchestration_router" "router" { + event_orchestration = pagerduty_event_orchestration.orch.id + + catch_all { + actions { + route_to = "unrouted" + } + } + set { + id = "start" + rule { + label = "dynamic routing rule 1" + condition { + expression = "event.summary matches part 'production'" + } + actions { + dynamic_route_to { + lookup_by = "service_id" + regex = ".*" + source = "event.custom_details.pd_service_id" + } + route_to = %s + } + } + rule { + label = "static routing rule 1" + actions { + route_to = "P1B2C23" + } + } + + } + } + `, routeTo) + return fmt.Sprintf("%s%s", createBaseConfig(t, ep, s, o), routerConfig) +} + func testAccCheckPagerDutyEventOrchestrationRouterConfigNoRules(t, ep, s, o string) string { return fmt.Sprintf("%s%s", createBaseConfig(t, ep, s, o), `resource "pagerduty_event_orchestration_router" "router" {