From 3b74b81bf1be5490c90d14955ed97d92e35e2a27 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Mon, 8 Jul 2024 12:35:55 -0400 Subject: [PATCH] add `grafana_oncall_user_notification_rule` resource (#1653) * add `grafana_oncall_user_notification_rule` resource * WIP * WIP * WIP * WIP * docs + update `github.com/grafana/amixr-api-go-client` package to latest version * linting * remove unused code * update `type` documentation * update tests * `go generate` * `make golangci-lint` * update tests * update tests * update tests * change test user_id * change test user id * update tests * update tests * update tests --- .../oncall_user_notification_rule.md | 111 ++++++ .../import.sh | 1 + .../resource.tf | 66 ++++ go.mod | 2 +- go.sum | 2 + .../oncall/resource_user_notification_rule.go | 329 ++++++++++++++++++ .../resource_user_notification_rule_test.go | 144 ++++++++ internal/resources/oncall/resources.go | 27 ++ 8 files changed, 681 insertions(+), 1 deletion(-) create mode 100644 docs/resources/oncall_user_notification_rule.md create mode 100644 examples/resources/grafana_oncall_user_notification_rule/import.sh create mode 100644 examples/resources/grafana_oncall_user_notification_rule/resource.tf create mode 100644 internal/resources/oncall/resource_user_notification_rule.go create mode 100644 internal/resources/oncall/resource_user_notification_rule_test.go diff --git a/docs/resources/oncall_user_notification_rule.md b/docs/resources/oncall_user_notification_rule.md new file mode 100644 index 000000000..a83ed2f44 --- /dev/null +++ b/docs/resources/oncall_user_notification_rule.md @@ -0,0 +1,111 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "grafana_oncall_user_notification_rule Resource - terraform-provider-grafana" +subcategory: "OnCall" +description: |- + HTTP API https://grafana.com/docs/oncall/latest/oncall-api-reference/personal_notification_rules/ + Note: you must be running Grafana OnCall >= v1.8.0 to use this resource. +--- + +# grafana_oncall_user_notification_rule (Resource) + +* [HTTP API](https://grafana.com/docs/oncall/latest/oncall-api-reference/personal_notification_rules/) + +**Note**: you must be running Grafana OnCall >= v1.8.0 to use this resource. + +## Example Usage + +```terraform +data "grafana_oncall_user" "my_user" { + provider = grafana.oncall + username = "my_username" +} + +resource "grafana_oncall_user_notification_rule" "my_user_step_1" { + provider = grafana.oncall + user_id = data.grafana_oncall_user.my_user.id + position = 0 + type = "notify_by_mobile_app" +} + +resource "grafana_oncall_user_notification_rule" "my_user_step_2" { + provider = grafana.oncall + user_id = data.grafana_oncall_user.my_user.id + position = 1 + duration = 600 # 10 minutes + type = "wait" +} + +resource "grafana_oncall_user_notification_rule" "my_user_step_3" { + provider = grafana.oncall + user_id = data.grafana_oncall_user.my_user.id + position = 2 + type = "notify_by_phone_call" +} + +resource "grafana_oncall_user_notification_rule" "my_user_step_4" { + provider = grafana.oncall + user_id = data.grafana_oncall_user.my_user.id + position = 3 + duration = 300 # 5 minutes + type = "wait" +} + +resource "grafana_oncall_user_notification_rule" "my_user_step_5" { + provider = grafana.oncall + user_id = data.grafana_oncall_user.my_user.id + position = 4 + type = "notify_by_slack" +} + +resource "grafana_oncall_user_notification_rule" "my_user_important_step_1" { + provider = grafana.oncall + user_id = data.grafana_oncall_user.my_user.id + important = true + position = 0 + type = "notify_by_mobile_app_critical" +} + +resource "grafana_oncall_user_notification_rule" "my_user_important_step_2" { + provider = grafana.oncall + user_id = data.grafana_oncall_user.my_user.id + important = true + position = 1 + duration = 300 # 5 minutes + type = "wait" +} + +resource "grafana_oncall_user_notification_rule" "my_user_important_step_3" { + provider = grafana.oncall + user_id = data.grafana_oncall_user.my_user.id + important = true + position = 2 + type = "notify_by_mobile_app_critical" +} +``` + + +## Schema + +### Required + +- `type` (String) The type of notification rule. Can be wait, notify_by_slack, notify_by_msteams, notify_by_sms, notify_by_phone_call, notify_by_telegram, notify_by_email, notify_by_mobile_app, notify_by_mobile_app_critical. NOTE: `notify_by_msteams` is only available for Grafana Cloud customers. +- `user_id` (String) User ID + +### Optional + +- `duration` (Number) A time in seconds to wait (when `type=wait`). Can be 60, 300, 900, 1800, 3600 +- `important` (Boolean) Boolean value which indicates if a rule is “important” +- `position` (Number) Personal notification rules execute one after another starting from position=0. A new escalation policy created with a position of an existing escalation policy will move the old one (and all following) down on the list. + +### Read-Only + +- `id` (String) The ID of this resource. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import grafana_oncall_user_notification_rule.name "{{ id }}" +``` diff --git a/examples/resources/grafana_oncall_user_notification_rule/import.sh b/examples/resources/grafana_oncall_user_notification_rule/import.sh new file mode 100644 index 000000000..b8cc29e94 --- /dev/null +++ b/examples/resources/grafana_oncall_user_notification_rule/import.sh @@ -0,0 +1 @@ +terraform import grafana_oncall_user_notification_rule.name "{{ id }}" diff --git a/examples/resources/grafana_oncall_user_notification_rule/resource.tf b/examples/resources/grafana_oncall_user_notification_rule/resource.tf new file mode 100644 index 000000000..cf655c7f3 --- /dev/null +++ b/examples/resources/grafana_oncall_user_notification_rule/resource.tf @@ -0,0 +1,66 @@ +data "grafana_oncall_user" "my_user" { + provider = grafana.oncall + username = "my_username" +} + +resource "grafana_oncall_user_notification_rule" "my_user_step_1" { + provider = grafana.oncall + user_id = data.grafana_oncall_user.my_user.id + position = 0 + type = "notify_by_mobile_app" +} + +resource "grafana_oncall_user_notification_rule" "my_user_step_2" { + provider = grafana.oncall + user_id = data.grafana_oncall_user.my_user.id + position = 1 + duration = 600 # 10 minutes + type = "wait" +} + +resource "grafana_oncall_user_notification_rule" "my_user_step_3" { + provider = grafana.oncall + user_id = data.grafana_oncall_user.my_user.id + position = 2 + type = "notify_by_phone_call" +} + +resource "grafana_oncall_user_notification_rule" "my_user_step_4" { + provider = grafana.oncall + user_id = data.grafana_oncall_user.my_user.id + position = 3 + duration = 300 # 5 minutes + type = "wait" +} + +resource "grafana_oncall_user_notification_rule" "my_user_step_5" { + provider = grafana.oncall + user_id = data.grafana_oncall_user.my_user.id + position = 4 + type = "notify_by_slack" +} + +resource "grafana_oncall_user_notification_rule" "my_user_important_step_1" { + provider = grafana.oncall + user_id = data.grafana_oncall_user.my_user.id + important = true + position = 0 + type = "notify_by_mobile_app_critical" +} + +resource "grafana_oncall_user_notification_rule" "my_user_important_step_2" { + provider = grafana.oncall + user_id = data.grafana_oncall_user.my_user.id + important = true + position = 1 + duration = 300 # 5 minutes + type = "wait" +} + +resource "grafana_oncall_user_notification_rule" "my_user_important_step_3" { + provider = grafana.oncall + user_id = data.grafana_oncall_user.my_user.id + important = true + position = 2 + type = "notify_by_mobile_app_critical" +} diff --git a/go.mod b/go.mod index 1538f9a10..e3a3fa7b2 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/fatih/color v1.17.0 github.com/go-openapi/runtime v0.28.0 github.com/go-openapi/strfmt v0.23.0 - github.com/grafana/amixr-api-go-client v0.0.12-0.20240410110211-c9f68db085c4 // main branch + github.com/grafana/amixr-api-go-client v0.0.12 // main branch github.com/grafana/grafana-com-public-clients/go/gcom v0.0.0-20240322153219-42c6a1d2bcab github.com/grafana/grafana-openapi-client-go v0.0.0-20240523010106-657d101fcbd9 github.com/grafana/machine-learning-go-client v0.7.0 diff --git a/go.sum b/go.sum index 24a4edfbf..d8c9d9931 100644 --- a/go.sum +++ b/go.sum @@ -136,6 +136,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grafana/amixr-api-go-client v0.0.12-0.20240410110211-c9f68db085c4 h1:e7cZfDiNodjQn63be9m8zfnvMEQAMqHVFswjcbdlspk= github.com/grafana/amixr-api-go-client v0.0.12-0.20240410110211-c9f68db085c4/go.mod h1:N6x26XUrM5zGtK5zL5vNJnAn2JFMxLFPPLTw/6pDkFE= +github.com/grafana/amixr-api-go-client v0.0.12 h1:oEHZTBhxoZ35EsfeccZBJGPKhZUVOmdSir3WWnSJMLc= +github.com/grafana/amixr-api-go-client v0.0.12/go.mod h1:N6x26XUrM5zGtK5zL5vNJnAn2JFMxLFPPLTw/6pDkFE= github.com/grafana/grafana-com-public-clients/go/gcom v0.0.0-20240322153219-42c6a1d2bcab h1:/5R8NO996/keDkZqKXEkU3/QgFs1wzChKYkakjsBpRk= github.com/grafana/grafana-com-public-clients/go/gcom v0.0.0-20240322153219-42c6a1d2bcab/go.mod h1:6sYY1qgwYfSDNQhKQA0tar8Oc38cIGfyqwejhxoOsPs= github.com/grafana/grafana-openapi-client-go v0.0.0-20240523010106-657d101fcbd9 h1:lOumw0RmkvKsTRMm6e5x2x6EbtyTeIKhy8ZJaK1KW9w= diff --git a/internal/resources/oncall/resource_user_notification_rule.go b/internal/resources/oncall/resource_user_notification_rule.go new file mode 100644 index 000000000..51d99622b --- /dev/null +++ b/internal/resources/oncall/resource_user_notification_rule.go @@ -0,0 +1,329 @@ +package oncall + +import ( + "context" + "fmt" + "net/http" + "strings" + + onCallAPI "github.com/grafana/amixr-api-go-client" + "github.com/grafana/terraform-provider-grafana/v3/internal/common" + + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + resourceUserNotificationRuleName = "grafana_oncall_user_notification_rule" + + userNotificationRuleTypeOptions = []string{ + "wait", + "notify_by_slack", + "notify_by_msteams", + "notify_by_sms", + "notify_by_phone_call", + "notify_by_telegram", + "notify_by_email", + "notify_by_mobile_app", + "notify_by_mobile_app_critical", + } + userNotificationRuleTypeOptionsVerbal = strings.Join(userNotificationRuleTypeOptions, ", ") + + // https://github.com/grafana/oncall/blob/6e0bebaa110a802187254321bd9c3c138fc590b6/engine/apps/base/models/user_notification_policy.py#L123-L129 + userNotificationRuleDurationOptions = []int64{ + 60, // 1 minute + 60 * 5, // 5 minutes + 60 * 15, // 15 minutes + 60 * 30, // 30 minutes + 60 * 60, // 1 hour + } + userNotificationRuleDurationOptionsVerbal = strings.Trim(strings.Join(strings.Fields(fmt.Sprint(userNotificationRuleDurationOptions)), ", "), "[]") + + // Check interface + _ resource.ResourceWithImportState = (*userNotificationRuleResource)(nil) +) + +func resourceUserNotificationRule() *common.Resource { + return common.NewResource( + common.CategoryOnCall, + resourceUserNotificationRuleName, + resourceID, + &userNotificationRuleResource{}, + ).WithLister(oncallListerFunction(listUserNotificationRules)) +} + +type resourceUserNotificationRuleModel struct { + ID types.String `tfsdk:"id"` + UserID types.String `tfsdk:"user_id"` + Position types.Int64 `tfsdk:"position"` + Duration types.Int64 `tfsdk:"duration"` + Important types.Bool `tfsdk:"important"` + Type types.String `tfsdk:"type"` +} + +type userNotificationRuleResource struct { + basePluginFrameworkResource +} + +func (r *userNotificationRuleResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = resourceUserNotificationRuleName +} + +func (r *userNotificationRuleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "user_id": schema.StringAttribute{ + MarkdownDescription: "User ID", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "position": schema.Int64Attribute{ + MarkdownDescription: "Personal notification rules execute one after another starting from position=0. A new escalation policy created with a position of an existing escalation policy will move the old one (and all following) down on the list.", + Optional: true, + }, + "duration": schema.Int64Attribute{ + MarkdownDescription: fmt.Sprintf("A time in seconds to wait (when `type=wait`). Can be %s", userNotificationRuleDurationOptionsVerbal), + Optional: true, + Validators: []validator.Int64{ + int64validator.OneOf(userNotificationRuleDurationOptions...), + }, + }, + "important": schema.BoolAttribute{ + MarkdownDescription: "Boolean value which indicates if a rule is “important”", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + }, + "type": schema.StringAttribute{ + MarkdownDescription: fmt.Sprintf("The type of notification rule. Can be %s. NOTE: `notify_by_msteams` is only available for Grafana Cloud customers.", userNotificationRuleTypeOptionsVerbal), + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf(userNotificationRuleTypeOptions...), + }, + }, + }, + MarkdownDescription: ` +* [HTTP API](https://grafana.com/docs/oncall/latest/oncall-api-reference/personal_notification_rules/) + +**Note**: you must be running Grafana OnCall >= v1.8.0 to use this resource. +`, + } +} + +func listUserNotificationRules(client *onCallAPI.Client, listOptions onCallAPI.ListOptions) (ids []string, nextPage *string, err error) { + resp, _, err := client.UserNotificationRules.ListUserNotificationRules(&onCallAPI.ListUserNotificationRuleOptions{ListOptions: listOptions}) + if err != nil { + return nil, nil, err + } + for _, i := range resp.UserNotificationRules { + ids = append(ids, i.ID) + } + return ids, resp.Next, nil +} + +func (r *userNotificationRuleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + data, diags := r.readFromID(ctx, req.ID) + if diags != nil { + resp.Diagnostics = diags + return + } + if data == nil { + resp.Diagnostics.AddError("Resource not found", "Resource not found") + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, data)...) +} + +func (r *userNotificationRuleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Read Terraform state data into the model + var data resourceUserNotificationRuleModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + // Read from API + readData, diags := r.readFromID(ctx, data.ID.ValueString()) + if diags != nil { + resp.Diagnostics = diags + return + } + if readData == nil { + resp.State.RemoveResource(ctx) + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, readData)...) +} + +func (r *userNotificationRuleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + if r.client == nil { + resp.Diagnostics.AddError("client not configured", "client not configured") + return + } + + // Read Terraform plan data into the model + var data resourceUserNotificationRuleModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + createOptions := &onCallAPI.CreateUserNotificationRuleOptions{ + UserId: data.UserID.ValueString(), + Type: data.Type.ValueString(), + ManualOrder: true, + } + + if !data.Position.IsNull() { + p := int(data.Position.ValueInt64()) + createOptions.Position = &p + } + + if data.Duration.ValueInt64() > 0 { + d := int(data.Duration.ValueInt64()) + createOptions.Duration = &d + } + + if data.Important.ValueBool() { + createOptions.Important = data.Important.ValueBool() + } + + userNotificationRule, _, err := r.client.UserNotificationRules.CreateUserNotificationRule(createOptions) + if err != nil { + resp.Diagnostics.AddError("Unable to Create Resource", err.Error()) + return + } + + // Read created resource + readData, diags := r.readFromID(ctx, userNotificationRule.ID) + if diags != nil { + resp.Diagnostics = diags + return + } + if readData == nil { + resp.Diagnostics.AddError("Unable to read created resource", "Resource not found") + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, readData)...) +} + +func (r *userNotificationRuleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + if r.client == nil { + resp.Diagnostics.AddError("client not configured", "client not configured") + return + } + + // Read Terraform plan data into the model + var data resourceUserNotificationRuleModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + updateOptions := &onCallAPI.UpdateUserNotificationRuleOptions{ + Type: data.Type.ValueString(), + ManualOrder: true, + } + + if !data.Position.IsNull() { + p := int(data.Position.ValueInt64()) + updateOptions.Position = &p + } + + if data.Duration.ValueInt64() == 0 { + updateOptions.Duration = nil + } else { + d := int(data.Duration.ValueInt64()) + updateOptions.Duration = &d + } + + userNotificationRule, _, err := r.client.UserNotificationRules.UpdateUserNotificationRule(data.ID.ValueString(), updateOptions) + if err != nil { + resp.Diagnostics.AddError("Unable to Update Resource", err.Error()) + return + } + + // Read updated resource + readData, diags := r.readFromID(ctx, userNotificationRule.ID) + if diags != nil { + resp.Diagnostics = diags + return + } + if readData == nil { + resp.Diagnostics.AddError("Unable to read updated resource", "Resource not found") + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, readData)...) +} + +func (r *userNotificationRuleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + if r.client == nil { + resp.Diagnostics.AddError("client not configured", "client not configured") + return + } + + // Read Terraform plan data into the model + var data resourceUserNotificationRuleModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // DELETE + _, err := r.client.UserNotificationRules.DeleteUserNotificationRule(data.ID.ValueString(), &onCallAPI.DeleteUserNotificationRuleOptions{}) + if err != nil { + resp.Diagnostics.AddError("Unable to Delete Resource", err.Error()) + } +} + +func (r *userNotificationRuleResource) readFromID(_ context.Context, id string) (*resourceUserNotificationRuleModel, diag.Diagnostics) { + if r.client == nil { + return nil, diag.Diagnostics{diag.NewErrorDiagnostic("client not configured", "client not configured")} + } + + // GET + ruleResp, httpResp, err := r.client.UserNotificationRules.GetUserNotificationRule(id, &onCallAPI.GetUserNotificationRuleOptions{}) + + if httpResp.StatusCode == http.StatusNotFound { + return nil, nil + } + + if err != nil { + return nil, diag.Diagnostics{diag.NewErrorDiagnostic("Unable to read resource", err.Error())} + } + + data := &resourceUserNotificationRuleModel{ + ID: types.StringValue(id), + UserID: types.StringValue(ruleResp.UserId), + Position: types.Int64Value(int64(ruleResp.Position)), + Important: types.BoolValue(ruleResp.Important), + Type: types.StringValue(ruleResp.Type), + } + + if ruleResp.Duration == 0 { + data.Duration = types.Int64Null() + } else { + data.Duration = types.Int64Value(int64(ruleResp.Duration)) + } + + return data, nil +} diff --git a/internal/resources/oncall/resource_user_notification_rule_test.go b/internal/resources/oncall/resource_user_notification_rule_test.go new file mode 100644 index 000000000..6bcf9def0 --- /dev/null +++ b/internal/resources/oncall/resource_user_notification_rule_test.go @@ -0,0 +1,144 @@ +package oncall_test + +import ( + "fmt" + "testing" + + onCallAPI "github.com/grafana/amixr-api-go-client" + "github.com/grafana/terraform-provider-grafana/v3/internal/common" + "github.com/grafana/terraform-provider-grafana/v3/internal/testutils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccUserNotificationRule_basic(t *testing.T) { + testutils.CheckCloudInstanceTestsEnabled(t) + + var ( + // We need an actual user to test the resource + // This is a user created from my personal email, but it can be replaced by any existing user + userID = "joeyorlando" + resourceName = "grafana_oncall_user_notification_rule.test-acc-user_notification_rule" + + testSteps []resource.TestStep + + ruleTypes = []string{ + "wait", + "notify_by_slack", + "notify_by_msteams", + "notify_by_sms", + "notify_by_phone_call", + "notify_by_telegram", + "notify_by_email", + "notify_by_mobile_app", + "notify_by_mobile_app_critical", + } + ) + + for _, ruleType := range ruleTypes { + for _, important := range []bool{false, true} { + var ( + config string + testCheckFuncFunctions = []resource.TestCheckFunc{ + testAccCheckOnCallUserNotificationRuleResourceExists(resourceName), + // resource.TestCheckResourceAttr(resourceName, "user_id", userID), + resource.TestCheckResourceAttr(resourceName, "position", "1"), + resource.TestCheckResourceAttr(resourceName, "type", ruleType), + resource.TestCheckResourceAttr(resourceName, "important", fmt.Sprintf("%t", important)), + } + ) + + if ruleType == "wait" { + config = testAccOnCallUserNotificationRuleWait(userID, important) + testCheckFuncFunctions = append(testCheckFuncFunctions, resource.TestCheckResourceAttr(resourceName, "duration", "300")) + } else { + config = testAccOnCallUserNotificationRuleNotificationStep(ruleType, userID, important) + } + + testSteps = append(testSteps, resource.TestStep{ + Config: config, + Check: resource.ComposeTestCheckFunc(testCheckFuncFunctions...), + }) + } + } + + testSteps = append(testSteps, resource.TestStep{ + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }) + + resource.ParallelTest(t, resource.TestCase{ + ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckOnCallUserNotificationRuleResourceDestroy, + Steps: testSteps, + }) +} + +func testAccCheckOnCallUserNotificationRuleResourceDestroy(s *terraform.State) error { + client := testutils.Provider.Meta().(*common.Client).OnCallClient + for _, r := range s.RootModule().Resources { + if r.Type != "grafana_oncall_user_notification_rule" { + continue + } + + if _, _, err := client.UserNotificationRules.GetUserNotificationRule(r.Primary.ID, &onCallAPI.GetUserNotificationRuleOptions{}); err == nil { + return fmt.Errorf("UserNotificationRule still exists") + } + } + return nil +} + +func testAccOnCallUserNotificationRuleWait(userName string, important bool) string { + return fmt.Sprintf(` +data "grafana_oncall_user" "user" { + username = "%s" +} + +resource "grafana_oncall_user_notification_rule" "test-acc-user_notification_rule" { + user_id = data.grafana_oncall_user.user.id + type = "wait" + position = 1 + duration = 300 + important = %t +} +`, userName, important) +} + +func testAccOnCallUserNotificationRuleNotificationStep(ruleType, userName string, important bool) string { + return fmt.Sprintf(` +data "grafana_oncall_user" "user" { + username = "%s" +} + +resource "grafana_oncall_user_notification_rule" "test-acc-user_notification_rule" { + user_id = data.grafana_oncall_user.user.id + type = "%s" + position = 1 + important = %t +} +`, userName, ruleType, important) +} + +func testAccCheckOnCallUserNotificationRuleResourceExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + if rs.Primary.ID == "" { + return fmt.Errorf("No UserNotificationRule ID is set") + } + + client := testutils.Provider.Meta().(*common.Client).OnCallClient + + found, _, err := client.UserNotificationRules.GetUserNotificationRule(rs.Primary.ID, &onCallAPI.GetUserNotificationRuleOptions{}) + if err != nil { + return err + } + if found.ID != rs.Primary.ID { + return fmt.Errorf("UserNotificationRule policy not found: %v - %v", rs.Primary.ID, found) + } + return nil + } +} diff --git a/internal/resources/oncall/resources.go b/internal/resources/oncall/resources.go index d32b5cd73..a971818fc 100644 --- a/internal/resources/oncall/resources.go +++ b/internal/resources/oncall/resources.go @@ -2,9 +2,11 @@ package oncall import ( "context" + "fmt" onCallAPI "github.com/grafana/amixr-api-go-client" "github.com/grafana/terraform-provider-grafana/v3/internal/common" + "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -12,6 +14,30 @@ import ( // All on-call resources have a single string ID format var resourceID = common.NewResourceID(common.StringIDField("id")) +type basePluginFrameworkResource struct { + client *onCallAPI.Client +} + +func (r *basePluginFrameworkResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Configure is called multiple times (sometimes when ProviderData is not yet available), we only want to configure once + if req.ProviderData == nil || r.client != nil { + return + } + + client, ok := req.ProviderData.(*common.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *common.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client.OnCallClient +} + type crudWithClientFunc func(ctx context.Context, d *schema.ResourceData, client *onCallAPI.Client) diag.Diagnostics func withClient[T schema.CreateContextFunc | schema.UpdateContextFunc | schema.ReadContextFunc | schema.DeleteContextFunc](f crudWithClientFunc) T { @@ -43,4 +69,5 @@ var Resources = []*common.Resource{ resourceOnCallShift(), resourceSchedule(), resourceOutgoingWebhook(), + resourceUserNotificationRule(), }