From b4fa5eabd45431ba485cf07da31b98d287bfd208 Mon Sep 17 00:00:00 2001 From: Julien Duchesne Date: Thu, 1 Aug 2024 20:47:05 -0400 Subject: [PATCH] Config Generation: Fix oncall Currently, we have errors with two oncall resources: - Schedule: `web` type schedules are fetched but they can't be managed with TF - Escalation: Mutually exclusive attributes are being set. With this change, all `nil` attributes won't be set in the TF state (rather than a zero value) --- .../resources/oncall/resource_escalation.go | 40 +++- .../oncall/resource_escalation_test.go | 15 ++ .../resources/oncall/resource_schedule.go | 4 + .../oncall/resource_schedule_test.go | 15 ++ pkg/generate/generate_test.go | 171 ++++++++++++++---- .../generate/oncall-resources/imports.tf.tmpl | 19 ++ .../generate/oncall-resources/provider.tf | 15 ++ .../oncall-resources/resources.tf.tmpl | 29 +++ 8 files changed, 258 insertions(+), 50 deletions(-) create mode 100644 pkg/generate/testdata/generate/oncall-resources/imports.tf.tmpl create mode 100644 pkg/generate/testdata/generate/oncall-resources/provider.tf create mode 100644 pkg/generate/testdata/generate/oncall-resources/resources.tf.tmpl diff --git a/internal/resources/oncall/resource_escalation.go b/internal/resources/oncall/resource_escalation.go index d402da49e..37a5daec5 100644 --- a/internal/resources/oncall/resource_escalation.go +++ b/internal/resources/oncall/resource_escalation.go @@ -363,16 +363,36 @@ func resourceEscalationRead(ctx context.Context, d *schema.ResourceData, client d.Set("escalation_chain_id", escalation.EscalationChainId) d.Set("position", escalation.Position) d.Set("type", escalation.Type) - d.Set("duration", escalation.Duration) - d.Set("notify_on_call_from_schedule", escalation.NotifyOnCallFromSchedule) - d.Set("persons_to_notify", escalation.PersonsToNotify) - d.Set("persons_to_notify_next_each_time", escalation.PersonsToNotifyEachTime) - d.Set("notify_to_team_members", escalation.TeamToNotify) - d.Set("group_to_notify", escalation.GroupToNotify) - d.Set("action_to_trigger", escalation.ActionToTrigger) - d.Set("important", escalation.Important) - d.Set("notify_if_time_from", escalation.NotifyIfTimeFrom) - d.Set("notify_if_time_to", escalation.NotifyIfTimeTo) + if escalation.Duration != nil { + d.Set("duration", escalation.Duration) + } + if escalation.NotifyOnCallFromSchedule != nil { + d.Set("notify_on_call_from_schedule", escalation.NotifyOnCallFromSchedule) + } + if escalation.PersonsToNotify != nil { + d.Set("persons_to_notify", escalation.PersonsToNotify) + } + if escalation.PersonsToNotifyEachTime != nil { + d.Set("persons_to_notify_next_each_time", escalation.PersonsToNotifyEachTime) + } + if escalation.TeamToNotify != nil { + d.Set("notify_to_team_members", escalation.TeamToNotify) + } + if escalation.GroupToNotify != nil { + d.Set("group_to_notify", escalation.GroupToNotify) + } + if escalation.ActionToTrigger != nil { + d.Set("action_to_trigger", escalation.ActionToTrigger) + } + if escalation.Important != nil { + d.Set("important", escalation.Important) + } + if escalation.NotifyIfTimeFrom != nil { + d.Set("notify_if_time_from", escalation.NotifyIfTimeFrom) + } + if escalation.NotifyIfTimeTo != nil { + d.Set("notify_if_time_to", escalation.NotifyIfTimeTo) + } return nil } diff --git a/internal/resources/oncall/resource_escalation_test.go b/internal/resources/oncall/resource_escalation_test.go index 7e060e0e7..33b9acc8c 100644 --- a/internal/resources/oncall/resource_escalation_test.go +++ b/internal/resources/oncall/resource_escalation_test.go @@ -40,6 +40,21 @@ func TestAccOnCallEscalation_basic(t *testing.T) { resource.TestCheckResourceAttrSet("grafana_oncall_escalation.test-acc-escalation-policy-team", "notify_to_team_members"), ), }, + { + ImportState: true, + ResourceName: "grafana_oncall_escalation.test-acc-escalation", + ImportStateVerify: true, + }, + { + ImportState: true, + ResourceName: "grafana_oncall_escalation.test-acc-escalation-repeat", + ImportStateVerify: true, + }, + { + ImportState: true, + ResourceName: "grafana_oncall_escalation.test-acc-escalation-policy-team", + ImportStateVerify: true, + }, }, }) } diff --git a/internal/resources/oncall/resource_schedule.go b/internal/resources/oncall/resource_schedule.go index 546549a5e..d32af8cf7 100644 --- a/internal/resources/oncall/resource_schedule.go +++ b/internal/resources/oncall/resource_schedule.go @@ -3,6 +3,7 @@ package oncall import ( "context" "net/http" + "slices" "strings" onCallAPI "github.com/grafana/amixr-api-go-client" @@ -114,6 +115,9 @@ func listSchedules(client *onCallAPI.Client, listOptions onCallAPI.ListOptions) return nil, nil, err } for _, i := range resp.Schedules { + if !slices.Contains(scheduleTypeOptions, i.Type) { + continue + } ids = append(ids, i.ID) } return ids, resp.Next, nil diff --git a/internal/resources/oncall/resource_schedule_test.go b/internal/resources/oncall/resource_schedule_test.go index 222687def..5876cfdc7 100644 --- a/internal/resources/oncall/resource_schedule_test.go +++ b/internal/resources/oncall/resource_schedule_test.go @@ -28,6 +28,11 @@ func TestAccOnCallSchedule_basic(t *testing.T) { resource.TestCheckResourceAttr("grafana_oncall_schedule.test-acc-schedule", "enable_web_overrides", "false"), ), }, + { + ImportState: true, + ResourceName: "grafana_oncall_schedule.test-acc-schedule", + ImportStateVerify: true, + }, { Config: testAccOnCallScheduleConfigOverrides(scheduleName, true), Check: resource.ComposeTestCheckFunc( @@ -35,6 +40,11 @@ func TestAccOnCallSchedule_basic(t *testing.T) { resource.TestCheckResourceAttr("grafana_oncall_schedule.test-acc-schedule", "enable_web_overrides", "true"), ), }, + { + ImportState: true, + ResourceName: "grafana_oncall_schedule.test-acc-schedule", + ImportStateVerify: true, + }, { Config: testAccOnCallScheduleConfigOverrides(scheduleName, false), Check: resource.ComposeTestCheckFunc( @@ -42,6 +52,11 @@ func TestAccOnCallSchedule_basic(t *testing.T) { resource.TestCheckResourceAttr("grafana_oncall_schedule.test-acc-schedule", "enable_web_overrides", "false"), ), }, + { + ImportState: true, + ResourceName: "grafana_oncall_schedule.test-acc-schedule", + ImportStateVerify: true, + }, }, }) } diff --git a/pkg/generate/generate_test.go b/pkg/generate/generate_test.go index 3365ceeae..a38170c5f 100644 --- a/pkg/generate/generate_test.go +++ b/pkg/generate/generate_test.go @@ -330,54 +330,145 @@ func TestAccGenerate_RestrictedPermissions(t *testing.T) { tc.Run(t) } -func TestAccGenerate_CloudInstance(t *testing.T) { +func TestAccGenerate_SMCheck(t *testing.T) { testutils.CheckCloudInstanceTestsEnabled(t) - // Install Terraform to a temporary directory to avoid reinstalling it for each test case. - installDir := t.TempDir() - randomString := acctest.RandString(10) + var smCheckID string - cases := []generateTestCase{ - { - name: "sm-check", - config: testutils.TestAccExampleWithReplace(t, "resources/grafana_synthetic_monitoring_check/http_basic.tf", map[string]string{ - `"HTTP Defaults"`: strconv.Quote(randomString), - }), - stateCheck: func(s *terraform.State) error { - checkResource, ok := s.RootModule().Resources["grafana_synthetic_monitoring_check.http"] - if !ok { - return fmt.Errorf("expected resource 'grafana_synthetic_monitoring_check.http' to be present") - } - smCheckID = checkResource.Primary.ID - return nil - }, - generateConfig: func(cfg *generate.Config) { - cfg.Grafana = &generate.GrafanaConfig{ - URL: os.Getenv("GRAFANA_URL"), - Auth: os.Getenv("GRAFANA_AUTH"), - SMURL: os.Getenv("GRAFANA_SM_URL"), - SMAccessToken: os.Getenv("GRAFANA_SM_ACCESS_TOKEN"), - } - cfg.IncludeResources = []string{"grafana_synthetic_monitoring_check._" + smCheckID} - }, - check: func(t *testing.T, tempDir string) { - templateAttrs := map[string]string{ - "ID": smCheckID, - "Job": randomString, - } - assertFilesWithTemplating(t, tempDir, "testdata/generate/sm-check", []string{ - ".terraform", - ".terraform.lock.hcl", - }, templateAttrs) - }, + tc := generateTestCase{ + name: "sm-check", + config: testutils.TestAccExampleWithReplace(t, "resources/grafana_synthetic_monitoring_check/http_basic.tf", map[string]string{ + `"HTTP Defaults"`: strconv.Quote(randomString), + }), + stateCheck: func(s *terraform.State) error { + checkResource, ok := s.RootModule().Resources["grafana_synthetic_monitoring_check.http"] + if !ok { + return fmt.Errorf("expected resource 'grafana_synthetic_monitoring_check.http' to be present") + } + smCheckID = checkResource.Primary.ID + return nil + }, + generateConfig: func(cfg *generate.Config) { + cfg.Grafana = &generate.GrafanaConfig{ + URL: os.Getenv("GRAFANA_URL"), + Auth: os.Getenv("GRAFANA_AUTH"), + SMURL: os.Getenv("GRAFANA_SM_URL"), + SMAccessToken: os.Getenv("GRAFANA_SM_ACCESS_TOKEN"), + } + cfg.IncludeResources = []string{"grafana_synthetic_monitoring_check._" + smCheckID} + }, + check: func(t *testing.T, tempDir string) { + templateAttrs := map[string]string{ + "ID": smCheckID, + "Job": randomString, + } + assertFilesWithTemplating(t, tempDir, "testdata/generate/sm-check", []string{ + ".terraform", + ".terraform.lock.hcl", + }, templateAttrs) }, } - for _, tc := range cases { - tc.tfInstallDir = installDir - tc.Run(t) + tc.Run(t) +} + +func TestAccGenerate_OnCall(t *testing.T) { + testutils.CheckCloudInstanceTestsEnabled(t) + + randomString := acctest.RandString(10) + + tfConfig := fmt.Sprintf(` + resource "grafana_oncall_integration" "test" { + name = "%[1]s" + type = "grafana" + default_route {} } + + resource "grafana_oncall_escalation_chain" "test"{ + name = "%[1]s" + } + + resource "grafana_oncall_escalation" "test" { + escalation_chain_id = grafana_oncall_escalation_chain.test.id + type = "wait" + duration = "300" + position = 0 + } + + resource "grafana_oncall_schedule" "test" { + name = "%[1]s" + type = "calendar" + time_zone = "America/New_York" + } + `, randomString) + + var ( + oncallIntegrationID string + oncallEscalationChainID string + oncallEscalationID string + oncallScheduleID string + ) + tc := generateTestCase{ + name: "oncall", + config: tfConfig, + generateConfig: func(cfg *generate.Config) { + cfg.Grafana = &generate.GrafanaConfig{ + URL: os.Getenv("GRAFANA_URL"), + Auth: os.Getenv("GRAFANA_AUTH"), + OnCallURL: "https://oncall-prod-us-central-0.grafana.net/oncall", + OnCallAccessToken: os.Getenv("GRAFANA_ONCALL_ACCESS_TOKEN"), + } + cfg.IncludeResources = []string{ + "grafana_oncall_integration._" + oncallIntegrationID, + "grafana_oncall_escalation_chain._" + oncallEscalationChainID, + "grafana_oncall_escalation._" + oncallEscalationID, + "grafana_oncall_schedule._" + oncallScheduleID, + } + }, + stateCheck: func(s *terraform.State) error { + integrationResource, ok := s.RootModule().Resources["grafana_oncall_integration.test"] + if !ok { + return fmt.Errorf("expected resource 'grafana_oncall_integration.test' to be present") + } + oncallIntegrationID = integrationResource.Primary.ID + + chainResource, ok := s.RootModule().Resources["grafana_oncall_escalation_chain.test"] + if !ok { + return fmt.Errorf("expected resource 'grafana_oncall_escalation_chain.test' to be present") + } + oncallEscalationChainID = chainResource.Primary.ID + + escalationResource, ok := s.RootModule().Resources["grafana_oncall_escalation.test"] + if !ok { + return fmt.Errorf("expected resource 'grafana_oncall_escalation.test' to be present") + } + oncallEscalationID = escalationResource.Primary.ID + + scheduleResource, ok := s.RootModule().Resources["grafana_oncall_schedule.test"] + if !ok { + return fmt.Errorf("expected resource 'grafana_oncall_schedule.test' to be present") + } + oncallScheduleID = scheduleResource.Primary.ID + + return nil + }, + check: func(t *testing.T, tempDir string) { + templateAttrs := map[string]string{ + "Name": randomString, + "IntegrationID": oncallIntegrationID, + "EscalationChainID": oncallEscalationChainID, + "EscalationID": oncallEscalationID, + "ScheduleID": oncallScheduleID, + } + assertFilesWithTemplating(t, tempDir, "testdata/generate/oncall-resources", []string{ + ".terraform", + ".terraform.lock.hcl", + }, templateAttrs) + }, + } + + tc.Run(t) } // assertFiles checks that all files in the "expectedFilesDir" directory match the files in the "gotFilesDir" directory. diff --git a/pkg/generate/testdata/generate/oncall-resources/imports.tf.tmpl b/pkg/generate/testdata/generate/oncall-resources/imports.tf.tmpl new file mode 100644 index 000000000..366a3f988 --- /dev/null +++ b/pkg/generate/testdata/generate/oncall-resources/imports.tf.tmpl @@ -0,0 +1,19 @@ +import { + to = grafana_oncall_escalation._{{ .EscalationID }} + id = "{{ .EscalationID }}" +} + +import { + to = grafana_oncall_escalation_chain._{{ .EscalationChainID }} + id = "{{ .EscalationChainID }}" +} + +import { + to = grafana_oncall_integration._{{ .IntegrationID }} + id = "{{ .IntegrationID }}" +} + +import { + to = grafana_oncall_schedule._{{ .ScheduleID }} + id = "{{ .ScheduleID }}" +} diff --git a/pkg/generate/testdata/generate/oncall-resources/provider.tf b/pkg/generate/testdata/generate/oncall-resources/provider.tf new file mode 100644 index 000000000..222bbbd30 --- /dev/null +++ b/pkg/generate/testdata/generate/oncall-resources/provider.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + grafana = { + source = "grafana/grafana" + version = "999.999.999" + } + } +} + +provider "grafana" { + url = "https://tfprovidertests.grafana.net/" + auth = "REDACTED" + oncall_url = "https://oncall-prod-us-central-0.grafana.net/oncall" + oncall_access_token = "REDACTED" +} diff --git a/pkg/generate/testdata/generate/oncall-resources/resources.tf.tmpl b/pkg/generate/testdata/generate/oncall-resources/resources.tf.tmpl new file mode 100644 index 000000000..caf82b9f9 --- /dev/null +++ b/pkg/generate/testdata/generate/oncall-resources/resources.tf.tmpl @@ -0,0 +1,29 @@ +# __generated__ by Terraform +# Please review these resources and move them into your main configuration files. + +# __generated__ by Terraform from "{{ .EscalationID }}" +resource "grafana_oncall_escalation" "_{{ .EscalationID }}" { + duration = 300 + escalation_chain_id = grafana_oncall_escalation_chain._{{ .EscalationChainID }}.id + position = 0 + type = "wait" +} + +# __generated__ by Terraform from "{{ .EscalationChainID }}" +resource "grafana_oncall_escalation_chain" "_{{ .EscalationChainID }}" { + name = "{{ .Name }}" +} + +# __generated__ by Terraform from "{{ .IntegrationID }}" +resource "grafana_oncall_integration" "_{{ .IntegrationID }}" { + name = "{{ .Name }}" + type = "grafana" +} + +# __generated__ by Terraform from "{{ .ScheduleID }}" +resource "grafana_oncall_schedule" "_{{ .ScheduleID }}" { + enable_web_overrides = false + name = "{{ .Name }}" + time_zone = "America/New_York" + type = "calendar" +}