diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 8bd6deb4f..dfbfb035b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -122,5 +122,5 @@ jobs:
SENTRY_TEST_PAGERDUTY_ORGANIZATION: ${{ secrets.SENTRY_TEST_PAGERDUTY_ORGANIZATION }}
SENTRY_TEST_VSTS_INSTALLATION_ID: ${{ secrets.SENTRY_TEST_VSTS_INSTALLATION_ID }}
SENTRY_TEST_VSTS_REPOSITORY_IDENTIFIER: ${{ secrets.SENTRY_TEST_VSTS_REPOSITORY_IDENTIFIER }}
- run: go test -v -cover ./internal/provider/
- timeout-minutes: 10
+ run: go test -v -cover -timeout 60m ./internal/provider/
+ timeout-minutes: 60
diff --git a/docs/data-sources/issue_alert.md b/docs/data-sources/issue_alert.md
index 50fdee6d0..9d216ef67 100644
--- a/docs/data-sources/issue_alert.md
+++ b/docs/data-sources/issue_alert.md
@@ -14,29 +14,10 @@ Sentry Issue Alert data source. See the [Sentry documentation](https://docs.sent
```terraform
# Retrieve an Issue Alert
-# URL format: https://sentry.io/organizations/[organization]/alerts/rules/[project]/[internal_id]/details/
data "sentry_issue_alert" "original" {
organization = "my-organization"
project = "my-project"
- internal_id = "42"
-}
-
-# Create a copy of an Issue Alert
-resource "sentry_issue_alert" "copy" {
- organization = data.sentry_issue_alert.original.organization
- project = data.sentry_issue_alert.original.project
-
- # Copy and modify attributes as necessary.
-
- name = "${data.sentry_issue_alert.original.name}-copy"
-
- action_match = data.sentry_issue_alert.original.action_match
- filter_match = data.sentry_issue_alert.original.filter_match
- frequency = data.sentry_issue_alert.original.frequency
-
- conditions = data.sentry_issue_alert.original.conditions
- filters = data.sentry_issue_alert.original.filters
- actions = data.sentry_issue_alert.original.actions
+ id = "42"
}
```
@@ -53,10 +34,380 @@ resource "sentry_issue_alert" "copy" {
- `action_match` (String) Trigger actions when an event is captured by Sentry and `any` or `all` of the specified conditions happen.
- `actions` (String) List of actions. In JSON string format.
+- `actions_v2` (Attributes List) A list of actions that take place when all required conditions and filters for the rule are met. (see [below for nested schema](#nestedatt--actions_v2))
- `conditions` (String) List of conditions. In JSON string format.
+- `conditions_v2` (Attributes List) A list of triggers that determine when the rule fires. (see [below for nested schema](#nestedatt--conditions_v2))
- `environment` (String) Perform issue alert in a specific environment.
- `filter_match` (String) A string determining which filters need to be true before any actions take place. Required when a value is provided for `filters`.
- `filters` (String) A list of filters that determine if a rule fires after the necessary conditions have been met. In JSON string format.
+- `filters_v2` (Attributes List) A list of filters that determine if a rule fires after the necessary conditions have been met. (see [below for nested schema](#nestedatt--filters_v2))
- `frequency` (Number) Perform actions at most once every `X` minutes for this issue.
- `name` (String) The issue alert name.
- `owner` (String) The ID of the team or user that owns the rule.
+
+
+### Nested Schema for `actions_v2`
+
+Read-Only:
+
+- `azure_devops_create_ticket` (Attributes) Create an Azure DevOps work item in `integration`. (see [below for nested schema](#nestedatt--actions_v2--azure_devops_create_ticket))
+- `discord_notify_service` (Attributes) Send a notification to the `server` Discord server in the channel with ID or URL: `channel_id` and show tags `tags` in the notification. (see [below for nested schema](#nestedatt--actions_v2--discord_notify_service))
+- `github_create_ticket` (Attributes) Create a GitHub issue in `integration`. (see [below for nested schema](#nestedatt--actions_v2--github_create_ticket))
+- `github_enterprise_create_ticket` (Attributes) Create a GitHub Enterprise issue in `integration`. (see [below for nested schema](#nestedatt--actions_v2--github_enterprise_create_ticket))
+- `jira_create_ticket` (Attributes) Create a Jira issue in `integration`. (see [below for nested schema](#nestedatt--actions_v2--jira_create_ticket))
+- `jira_server_create_ticket` (Attributes) Create a Jira Server issue in `integration`. (see [below for nested schema](#nestedatt--actions_v2--jira_server_create_ticket))
+- `msteams_notify_service` (Attributes) Send a notification to the `team` Team to `channel`. (see [below for nested schema](#nestedatt--actions_v2--msteams_notify_service))
+- `notify_email` (Attributes) Send a notification to `target_type` and if none can be found then send a notification to `fallthrough_type`. (see [below for nested schema](#nestedatt--actions_v2--notify_email))
+- `notify_event` (Attributes) Send a notification to all legacy integrations. (see [below for nested schema](#nestedatt--actions_v2--notify_event))
+- `notify_event_sentry_app` (Attributes) Send a notification to a Sentry app. (see [below for nested schema](#nestedatt--actions_v2--notify_event_sentry_app))
+- `notify_event_service` (Attributes) Send a notification via an integration. (see [below for nested schema](#nestedatt--actions_v2--notify_event_service))
+- `opsgenie_notify_team` (Attributes) Send a notification to Opsgenie account `account` and team `team` with `priority` priority. (see [below for nested schema](#nestedatt--actions_v2--opsgenie_notify_team))
+- `pagerduty_notify_service` (Attributes) Send a notification to PagerDuty account `account` and service `service` with `severity` severity. (see [below for nested schema](#nestedatt--actions_v2--pagerduty_notify_service))
+- `slack_notify_service` (Attributes) Send a notification to the `workspace` Slack workspace to `channel` (optionally, an ID: `channel_id`) and show tags `tags` and notes `notes` in notification. (see [below for nested schema](#nestedatt--actions_v2--slack_notify_service))
+
+
+### Nested Schema for `actions_v2.azure_devops_create_ticket`
+
+Read-Only:
+
+- `integration` (String)
+- `name` (String)
+- `work_item_type` (String)
+
+
+
+### Nested Schema for `actions_v2.discord_notify_service`
+
+Read-Only:
+
+- `channel_id` (String)
+- `name` (String)
+- `server` (String)
+- `tags` (Set of String)
+
+
+
+### Nested Schema for `actions_v2.github_create_ticket`
+
+Read-Only:
+
+- `assignee` (String)
+- `integration` (String)
+- `labels` (Set of String)
+- `name` (String)
+- `repo` (String)
+
+
+
+### Nested Schema for `actions_v2.github_enterprise_create_ticket`
+
+Read-Only:
+
+- `assignee` (String)
+- `integration` (String)
+- `labels` (Set of String)
+- `name` (String)
+- `repo` (String)
+
+
+
+### Nested Schema for `actions_v2.jira_create_ticket`
+
+Read-Only:
+
+- `integration` (String)
+- `issue_type` (String)
+- `name` (String)
+- `project` (String)
+
+
+
+### Nested Schema for `actions_v2.jira_server_create_ticket`
+
+Read-Only:
+
+- `integration` (String)
+- `issue_type` (String)
+- `name` (String)
+- `project` (String)
+
+
+
+### Nested Schema for `actions_v2.msteams_notify_service`
+
+Read-Only:
+
+- `channel` (String)
+- `channel_id` (String)
+- `name` (String)
+- `team` (String)
+
+
+
+### Nested Schema for `actions_v2.notify_email`
+
+Read-Only:
+
+- `fallthrough_type` (String)
+- `name` (String)
+- `target_identifier` (String)
+- `target_type` (String)
+
+
+
+### Nested Schema for `actions_v2.notify_event`
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `actions_v2.notify_event_sentry_app`
+
+Read-Only:
+
+- `name` (String)
+- `sentry_app_installation_uuid` (String)
+- `settings` (Map of String)
+
+
+
+### Nested Schema for `actions_v2.notify_event_service`
+
+Read-Only:
+
+- `name` (String)
+- `service` (String)
+
+
+
+### Nested Schema for `actions_v2.opsgenie_notify_team`
+
+Read-Only:
+
+- `account` (String)
+- `name` (String)
+- `priority` (String)
+- `team` (String)
+
+
+
+### Nested Schema for `actions_v2.pagerduty_notify_service`
+
+Read-Only:
+
+- `account` (String)
+- `name` (String)
+- `service` (String)
+- `severity` (String)
+
+
+
+### Nested Schema for `actions_v2.slack_notify_service`
+
+Read-Only:
+
+- `channel` (String)
+- `channel_id` (String)
+- `name` (String)
+- `notes` (String)
+- `tags` (Set of String)
+- `workspace` (String)
+
+
+
+
+### Nested Schema for `conditions_v2`
+
+Read-Only:
+
+- `event_frequency` (Attributes) When the `comparison_type` is `count`, the number of events in an issue is more than `value` in `interval`. When the `comparison_type` is `percent`, the number of events in an issue is `value` % higher in `interval` compared to `comparison_interval` ago. (see [below for nested schema](#nestedatt--conditions_v2--event_frequency))
+- `event_frequency_percent` (Attributes) When the `comparison_type` is `count`, the percent of sessions affected by an issue is more than `value` in `interval`. When the `comparison_type` is `percent`, the percent of sessions affected by an issue is `value` % higher in `interval` compared to `comparison_interval` ago. (see [below for nested schema](#nestedatt--conditions_v2--event_frequency_percent))
+- `event_unique_user_frequency` (Attributes) When the `comparison_type` is `count`, the number of users affected by an issue is more than `value` in `interval`. When the `comparison_type` is `percent`, the number of users affected by an issue is `value` % higher in `interval` compared to `comparison_interval` ago. (see [below for nested schema](#nestedatt--conditions_v2--event_unique_user_frequency))
+- `existing_high_priority_issue` (Attributes) Sentry marks an existing issue as high priority. (see [below for nested schema](#nestedatt--conditions_v2--existing_high_priority_issue))
+- `first_seen_event` (Attributes) A new issue is created. (see [below for nested schema](#nestedatt--conditions_v2--first_seen_event))
+- `new_high_priority_issue` (Attributes) Sentry marks a new issue as high priority. (see [below for nested schema](#nestedatt--conditions_v2--new_high_priority_issue))
+- `reappeared_event` (Attributes) The issue changes state from ignored to unresolved. (see [below for nested schema](#nestedatt--conditions_v2--reappeared_event))
+- `regression_event` (Attributes) The issue changes state from resolved to unresolved. (see [below for nested schema](#nestedatt--conditions_v2--regression_event))
+
+
+### Nested Schema for `conditions_v2.event_frequency`
+
+Read-Only:
+
+- `comparison_interval` (String)
+- `comparison_type` (String)
+- `interval` (String)
+- `name` (String)
+- `value` (Number)
+
+
+
+### Nested Schema for `conditions_v2.event_frequency_percent`
+
+Read-Only:
+
+- `comparison_interval` (String)
+- `comparison_type` (String)
+- `interval` (String)
+- `name` (String)
+- `value` (Number)
+
+
+
+### Nested Schema for `conditions_v2.event_unique_user_frequency`
+
+Read-Only:
+
+- `comparison_interval` (String)
+- `comparison_type` (String)
+- `interval` (String)
+- `name` (String)
+- `value` (Number)
+
+
+
+### Nested Schema for `conditions_v2.existing_high_priority_issue`
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `conditions_v2.first_seen_event`
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `conditions_v2.new_high_priority_issue`
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `conditions_v2.reappeared_event`
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `conditions_v2.regression_event`
+
+Read-Only:
+
+- `name` (String)
+
+
+
+
+### Nested Schema for `filters_v2`
+
+Read-Only:
+
+- `age_comparison` (Attributes) The issue is older or newer than `value` `time`. (see [below for nested schema](#nestedatt--filters_v2--age_comparison))
+- `assigned_to` (Attributes) The issue is assigned to no one, team, or member. (see [below for nested schema](#nestedatt--filters_v2--assigned_to))
+- `event_attribute` (Attributes) The event's `attribute` value `match` `value`. (see [below for nested schema](#nestedatt--filters_v2--event_attribute))
+- `issue_category` (Attributes) The issue's category is equal to `value`. (see [below for nested schema](#nestedatt--filters_v2--issue_category))
+- `issue_occurrences` (Attributes) The issue has happened at least `value` times (Note: this is approximate). (see [below for nested schema](#nestedatt--filters_v2--issue_occurrences))
+- `latest_adopted_release` (Attributes) The {oldest_or_newest} adopted release associated with the event's issue is {older_or_newer} than the latest adopted release in {environment}. (see [below for nested schema](#nestedatt--filters_v2--latest_adopted_release))
+- `latest_release` (Attributes) The event is from the latest release. (see [below for nested schema](#nestedatt--filters_v2--latest_release))
+- `level` (Attributes) The event's level is `match` `level`. (see [below for nested schema](#nestedatt--filters_v2--level))
+- `tagged_event` (Attributes) The event's tags match `key` `match` `value`. (see [below for nested schema](#nestedatt--filters_v2--tagged_event))
+
+
+### Nested Schema for `filters_v2.age_comparison`
+
+Read-Only:
+
+- `comparison_type` (String)
+- `name` (String)
+- `time` (String)
+- `value` (Number)
+
+
+
+### Nested Schema for `filters_v2.assigned_to`
+
+Read-Only:
+
+- `name` (String)
+- `target_identifier` (Number)
+- `target_type` (Number)
+
+
+
+### Nested Schema for `filters_v2.event_attribute`
+
+Read-Only:
+
+- `attribute` (String)
+- `match` (String)
+- `name` (String)
+- `value` (String)
+
+
+
+### Nested Schema for `filters_v2.issue_category`
+
+Read-Only:
+
+- `name` (String)
+- `value` (String)
+
+
+
+### Nested Schema for `filters_v2.issue_occurrences`
+
+Read-Only:
+
+- `name` (String)
+- `value` (Number)
+
+
+
+### Nested Schema for `filters_v2.latest_adopted_release`
+
+Read-Only:
+
+- `environment` (Number)
+- `name` (String)
+- `older_or_newer` (Number)
+- `oldest_or_newest` (Number)
+
+
+
+### Nested Schema for `filters_v2.latest_release`
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `filters_v2.level`
+
+Read-Only:
+
+- `level` (String)
+- `match` (String)
+- `name` (String)
+
+
+
+### Nested Schema for `filters_v2.tagged_event`
+
+Read-Only:
+
+- `key` (String)
+- `match` (String)
+- `name` (String)
+- `value` (String)
diff --git a/docs/resources/issue_alert.md b/docs/resources/issue_alert.md
index 91b06bb24..b598207d0 100644
--- a/docs/resources/issue_alert.md
+++ b/docs/resources/issue_alert.md
@@ -4,18 +4,14 @@ page_title: "sentry_issue_alert Resource - terraform-provider-sentry"
subcategory: ""
description: |-
Create an Issue Alert Rule for a Project. See the Sentry Documentation https://docs.sentry.io/api/alerts/create-an-issue-alert-rule-for-a-project/ for more information.
- Please note the following changes since v0.12.0:
- The attributes conditions, filters, and actions are in JSON string format. The types must match the Sentry API, otherwise Terraform will incorrectly detect a drift. Use parseint("string", 10) to convert a string to an integer. Avoid using jsonencode() as it is unable to distinguish between an integer and a float.The attribute internal_id has been removed. Use id instead.The attribute id is now the ID of the issue alert. Previously, it was a combination of the organization, project, and issue alert ID.
+ NOTE: Since v0.15.0, the conditions, filters, and actions attributes which are JSON strings have been deprecated in favor of conditions_v2, filters_v2, and actions_v2 which are lists of objects.
---
# sentry_issue_alert (Resource)
Create an Issue Alert Rule for a Project. See the [Sentry Documentation](https://docs.sentry.io/api/alerts/create-an-issue-alert-rule-for-a-project/) for more information.
-Please note the following changes since v0.12.0:
-- The attributes `conditions`, `filters`, and `actions` are in JSON string format. The types must match the Sentry API, otherwise Terraform will incorrectly detect a drift. Use `parseint("string", 10)` to convert a string to an integer. Avoid using `jsonencode()` as it is unable to distinguish between an integer and a float.
-- The attribute `internal_id` has been removed. Use `id` instead.
-- The attribute `id` is now the ID of the issue alert. Previously, it was a combination of the organization, project, and issue alert ID.
+**NOTE:** Since v0.15.0, the `conditions`, `filters`, and `actions` attributes which are JSON strings have been deprecated in favor of `conditions_v2`, `filters_v2`, and `actions_v2` which are lists of objects.
## Example Usage
@@ -29,81 +25,132 @@ resource "sentry_issue_alert" "main" {
filter_match = "any"
frequency = 30
- conditions = < for triage information"
- }
-]
-EOT
+ actions_v2 = [
+ {
+ slack_notify_service = {
+ workspace = data.sentry_organization_integration.slack.id
+ channel = "#warning"
+ tags = ["environment", "level"]
+ notes = "Please for triage information"
+ }
+ },
+ ]
// ...
}
@@ -200,6 +243,7 @@ EOT
# Send a Microsoft Teams notification
#
+# Retrieve a MS Teams integration
data "sentry_organization_integration" "msteams" {
organization = sentry_project.test.organization
@@ -207,16 +251,15 @@ data "sentry_organization_integration" "msteams" {
name = "My Team" # Name of your Microsoft Teams team
}
-resource "sentry_issue_alert" "msteams_alert" {
- actions = </
+ service = "my-service"
+ }
+ },
+ ]
// ...
}
#
-# Send a notification to a Sentry app with a custom webhook payload
+# Send a notification to a Sentry app
#
-resource "sentry_issue_alert" "notification_alert" {
- actions = <
+### Nested Schema for `actions_v2`
+
+Optional:
+
+- `azure_devops_create_ticket` (Attributes) Create an Azure DevOps work item in `integration`. (see [below for nested schema](#nestedatt--actions_v2--azure_devops_create_ticket))
+- `discord_notify_service` (Attributes) Send a notification to the `server` Discord server in the channel with ID or URL: `channel_id` and show tags `tags` in the notification. (see [below for nested schema](#nestedatt--actions_v2--discord_notify_service))
+- `github_create_ticket` (Attributes) Create a GitHub issue in `integration`. (see [below for nested schema](#nestedatt--actions_v2--github_create_ticket))
+- `github_enterprise_create_ticket` (Attributes) Create a GitHub Enterprise issue in `integration`. (see [below for nested schema](#nestedatt--actions_v2--github_enterprise_create_ticket))
+- `jira_create_ticket` (Attributes) Create a Jira issue in `integration`. (see [below for nested schema](#nestedatt--actions_v2--jira_create_ticket))
+- `jira_server_create_ticket` (Attributes) Create a Jira Server issue in `integration`. (see [below for nested schema](#nestedatt--actions_v2--jira_server_create_ticket))
+- `msteams_notify_service` (Attributes) Send a notification to the `team` Team to `channel`. (see [below for nested schema](#nestedatt--actions_v2--msteams_notify_service))
+- `notify_email` (Attributes) Send a notification to `target_type` and if none can be found then send a notification to `fallthrough_type`. (see [below for nested schema](#nestedatt--actions_v2--notify_email))
+- `notify_event` (Attributes) Send a notification to all legacy integrations. (see [below for nested schema](#nestedatt--actions_v2--notify_event))
+- `notify_event_sentry_app` (Attributes) Send a notification to a Sentry app. (see [below for nested schema](#nestedatt--actions_v2--notify_event_sentry_app))
+- `notify_event_service` (Attributes) Send a notification via an integration. (see [below for nested schema](#nestedatt--actions_v2--notify_event_service))
+- `opsgenie_notify_team` (Attributes) Send a notification to Opsgenie account `account` and team `team` with `priority` priority. (see [below for nested schema](#nestedatt--actions_v2--opsgenie_notify_team))
+- `pagerduty_notify_service` (Attributes) Send a notification to PagerDuty account `account` and service `service` with `severity` severity. (see [below for nested schema](#nestedatt--actions_v2--pagerduty_notify_service))
+- `slack_notify_service` (Attributes) Send a notification to the `workspace` Slack workspace to `channel` (optionally, an ID: `channel_id`) and show tags `tags` and notes `notes` in notification. (see [below for nested schema](#nestedatt--actions_v2--slack_notify_service))
+
+
+### Nested Schema for `actions_v2.azure_devops_create_ticket`
+
+Required:
+
+- `integration` (String) The integration ID.
+- `project` (String) The ID of the Azure DevOps project.
+- `work_item_type` (String) The type of work item to create.
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `actions_v2.discord_notify_service`
+
+Required:
+
+- `channel_id` (String) The ID of the channel to send the notification to. You must enter either a channel ID or a channel URL, not a channel name
+- `server` (String) The integration ID associated with the Discord server.
+
+Optional:
+
+- `tags` (Set of String) A string of tags to show in the notification.
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `actions_v2.github_create_ticket`
+
+Required:
+
+- `integration` (String) The integration ID associated with GitHub.
+- `repo` (String) The name of the repository to create the issue in.
+
+Optional:
+
+- `assignee` (String) The GitHub user to assign the issue to.
+- `labels` (Set of String) A list of labels to assign to the issue.
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `actions_v2.github_enterprise_create_ticket`
+
+Required:
+
+- `integration` (String) The integration ID associated with GitHub Enterprise.
+- `repo` (String) The name of the repository to create the issue in.
+
+Optional:
+
+- `assignee` (String) The GitHub user to assign the issue to.
+- `labels` (Set of String) A list of labels to assign to the issue.
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `actions_v2.jira_create_ticket`
+
+Required:
+
+- `integration` (String) The integration ID associated with Jira.
+- `issue_type` (String) The ID of the type of issue that the ticket should be created as.
+- `project` (String) The ID of the Jira project.
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `actions_v2.jira_server_create_ticket`
+
+Required:
+
+- `integration` (String) The integration ID associated with Jira Server.
+- `issue_type` (String) The ID of the type of issue that the ticket should be created as.
+- `project` (String) The ID of the Jira Server project.
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `actions_v2.msteams_notify_service`
+
+Required:
+
+- `channel` (String) The name of the channel to send the notification to.
+- `team` (String) The integration ID associated with the Microsoft Teams team.
+
+Read-Only:
+
+- `channel_id` (String)
+- `name` (String)
+
+
+
+### Nested Schema for `actions_v2.notify_email`
+
+Required:
+
+- `target_type` (String) Valid values are: `IssueOwners`, `Team`, and `Member`.
+
+Optional:
+
+- `fallthrough_type` (String) Who the notification should be sent to if there are no suggested assignees. Valid values are: `AllMembers`, `ActiveMembers`, and `NoOne`.
+- `target_identifier` (String) The ID of the Member or Team the notification should be sent to. Only required when `target_type` is `Team` or `Member`.
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `actions_v2.notify_event`
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `actions_v2.notify_event_sentry_app`
+
+Required:
+
+- `sentry_app_installation_uuid` (String)
+
+Optional:
+
+- `settings` (Map of String)
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `actions_v2.notify_event_service`
+
+Required:
+
+- `service` (String) The slug of the integration service. Sourced from `https://terraform-provider-sentry.sentry.io/settings/developer-settings//`.
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `actions_v2.opsgenie_notify_team`
+
+Required:
+
+- `account` (String)
+- `priority` (String)
+- `team` (String)
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `actions_v2.pagerduty_notify_service`
+
+Required:
+
+- `account` (String)
+- `service` (String)
+- `severity` (String)
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `actions_v2.slack_notify_service`
+
+Required:
+
+- `channel` (String) The name of the channel to send the notification to (e.g., #critical, Jane Schmidt).
+- `workspace` (String) The integration ID associated with the Slack workspace.
+
+Optional:
+
+- `notes` (String) Text to show alongside the notification. To @ a user, include their user id like `@`. To include a clickable link, format the link and title like ``.
+- `tags` (Set of String) A string of tags to show in the notification.
+
+Read-Only:
+
+- `channel_id` (String) The ID of the channel to send the notification to.
+- `name` (String)
+
+
+
+
+### Nested Schema for `conditions_v2`
+
+Optional:
+
+- `event_frequency` (Attributes) When the `comparison_type` is `count`, the number of events in an issue is more than `value` in `interval`. When the `comparison_type` is `percent`, the number of events in an issue is `value` % higher in `interval` compared to `comparison_interval` ago. (see [below for nested schema](#nestedatt--conditions_v2--event_frequency))
+- `event_frequency_percent` (Attributes) When the `comparison_type` is `count`, the percent of sessions affected by an issue is more than `value` in `interval`. When the `comparison_type` is `percent`, the percent of sessions affected by an issue is `value` % higher in `interval` compared to `comparison_interval` ago. (see [below for nested schema](#nestedatt--conditions_v2--event_frequency_percent))
+- `event_unique_user_frequency` (Attributes) When the `comparison_type` is `count`, the number of users affected by an issue is more than `value` in `interval`. When the `comparison_type` is `percent`, the number of users affected by an issue is `value` % higher in `interval` compared to `comparison_interval` ago. (see [below for nested schema](#nestedatt--conditions_v2--event_unique_user_frequency))
+- `existing_high_priority_issue` (Attributes) Sentry marks an existing issue as high priority. (see [below for nested schema](#nestedatt--conditions_v2--existing_high_priority_issue))
+- `first_seen_event` (Attributes) A new issue is created. (see [below for nested schema](#nestedatt--conditions_v2--first_seen_event))
+- `new_high_priority_issue` (Attributes) Sentry marks a new issue as high priority. (see [below for nested schema](#nestedatt--conditions_v2--new_high_priority_issue))
+- `reappeared_event` (Attributes) The issue changes state from ignored to unresolved. (see [below for nested schema](#nestedatt--conditions_v2--reappeared_event))
+- `regression_event` (Attributes) The issue changes state from resolved to unresolved. (see [below for nested schema](#nestedatt--conditions_v2--regression_event))
+
+
+### Nested Schema for `conditions_v2.event_frequency`
+
+Required:
+
+- `comparison_type` (String) Valid values are: `count`, and `percent`.
+- `value` (Number)
+
+Optional:
+
+- `comparison_interval` (String) `m` for minutes, `h` for hours, `d` for days, and `w` for weeks. Valid values are: `5m`, `15m`, `1h`, `1d`, `1w`, and `30d`.
+- `interval` (String) `m` for minutes, `h` for hours, `d` for days, and `w` for weeks. Valid values are: `1m`, `5m`, `15m`, `1h`, `1d`, `1w`, and `30d`.
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `conditions_v2.event_frequency_percent`
+
+Required:
+
+- `comparison_type` (String) Valid values are: `count`, and `percent`.
+- `interval` (String) `m` for minutes, `h` for hours. Valid values are: `5m`, `10m`, `30m`, and `1h`.
+- `value` (Number)
+
+Optional:
+
+- `comparison_interval` (String) `m` for minutes, `h` for hours, `d` for days, and `w` for weeks. Valid values are: `5m`, `15m`, `1h`, `1d`, `1w`, and `30d`.
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `conditions_v2.event_unique_user_frequency`
+
+Required:
+
+- `comparison_type` (String) Valid values are: `count`, and `percent`.
+- `value` (Number)
+
+Optional:
+
+- `comparison_interval` (String) `m` for minutes, `h` for hours, `d` for days, and `w` for weeks. Valid values are: `5m`, `15m`, `1h`, `1d`, `1w`, and `30d`.
+- `interval` (String) `m` for minutes, `h` for hours, `d` for days, and `w` for weeks. Valid values are: `1m`, `5m`, `15m`, `1h`, `1d`, `1w`, and `30d`.
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `conditions_v2.existing_high_priority_issue`
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `conditions_v2.first_seen_event`
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `conditions_v2.new_high_priority_issue`
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `conditions_v2.reappeared_event`
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `conditions_v2.regression_event`
+
+Read-Only:
+
+- `name` (String)
+
+
+
+
+### Nested Schema for `filters_v2`
+
+Optional:
+
+- `age_comparison` (Attributes) The issue is older or newer than `value` `time`. (see [below for nested schema](#nestedatt--filters_v2--age_comparison))
+- `assigned_to` (Attributes) The issue is assigned to no one, team, or member. (see [below for nested schema](#nestedatt--filters_v2--assigned_to))
+- `event_attribute` (Attributes) The event's `attribute` value `match` `value`. (see [below for nested schema](#nestedatt--filters_v2--event_attribute))
+- `issue_category` (Attributes) The issue's category is equal to `value`. (see [below for nested schema](#nestedatt--filters_v2--issue_category))
+- `issue_occurrences` (Attributes) The issue has happened at least `value` times (Note: this is approximate). (see [below for nested schema](#nestedatt--filters_v2--issue_occurrences))
+- `latest_adopted_release` (Attributes) The {oldest_or_newest} adopted release associated with the event's issue is {older_or_newer} than the latest adopted release in {environment}. (see [below for nested schema](#nestedatt--filters_v2--latest_adopted_release))
+- `latest_release` (Attributes) The event is from the latest release. (see [below for nested schema](#nestedatt--filters_v2--latest_release))
+- `level` (Attributes) The event's level is `match` `level`. (see [below for nested schema](#nestedatt--filters_v2--level))
+- `tagged_event` (Attributes) The event's tags match `key` `match` `value`. (see [below for nested schema](#nestedatt--filters_v2--tagged_event))
+
+
+### Nested Schema for `filters_v2.age_comparison`
+
+Required:
+
+- `comparison_type` (String) Valid values are: `older`, and `newer`.
+- `time` (String) Valid values are: `minute`, `hour`, `day`, and `week`.
+- `value` (Number)
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `filters_v2.assigned_to`
+
+Required:
+
+- `target_type` (String) Valid values are: `Unassigned`, `Team`, and `Member`.
+
+Optional:
+
+- `target_identifier` (String) The target's ID. Only required when `target_type` is `Team` or `Member`.
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `filters_v2.event_attribute`
+
+Required:
+
+- `attribute` (String) Valid values are: `message`, `platform`, `environment`, `type`, `error.handled`, `error.unhandled`, `error.main_thread`, `exception.type`, `exception.value`, `user.id`, `user.email`, `user.username`, `user.ip_address`, `http.method`, `http.url`, `http.status_code`, `sdk.name`, `stacktrace.code`, `stacktrace.module`, `stacktrace.filename`, `stacktrace.abs_path`, `stacktrace.package`, `unreal.crashtype`, `app.in_foreground`, `os.distribution_name`, and `os.distribution_version`.
+- `match` (String) The comparison operator. Valid values are: `CONTAINS`, `ENDS_WITH`, `EQUAL`, `GREATER_OR_EQUAL`, `GREATER`, `IS_SET`, `IS_IN`, `LESS_OR_EQUAL`, `LESS`, `NOT_CONTAINS`, `NOT_ENDS_WITH`, `NOT_EQUAL`, `NOT_SET`, `NOT_STARTS_WITH`, `NOT_IN`, and `STARTS_WITH`.
+
+Optional:
+
+- `value` (String)
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `filters_v2.issue_category`
+
+Required:
+
+- `value` (String) Valid values are: `Error`, `Performance`, `Profile`, `Cron`, `Replay`, `Feedback`, `Uptime`, and `Metric_Alert`.
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `filters_v2.issue_occurrences`
+
+Required:
+
+- `value` (Number)
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `filters_v2.latest_adopted_release`
+
+Required:
+
+- `environment` (String)
+- `older_or_newer` (String) Valid values are: `older`, and `newer`.
+- `oldest_or_newest` (String) Valid values are: `oldest`, and `newest`.
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `filters_v2.latest_release`
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `filters_v2.level`
+
+Required:
+
+- `level` (String) Valid values are: `sample`, `debug`, `info`, `warning`, `error`, and `fatal`.
+- `match` (String) The comparison operator. Valid values are: `EQUAL`, `GREATER_OR_EQUAL`, and `LESS_OR_EQUAL`.
+
+Read-Only:
+
+- `name` (String)
+
+
+
+### Nested Schema for `filters_v2.tagged_event`
+
+Required:
+
+- `key` (String) The tag.
+- `match` (String) The comparison operator. Valid values are: `CONTAINS`, `ENDS_WITH`, `EQUAL`, `GREATER_OR_EQUAL`, `GREATER`, `IS_SET`, `IS_IN`, `LESS_OR_EQUAL`, `LESS`, `NOT_CONTAINS`, `NOT_ENDS_WITH`, `NOT_EQUAL`, `NOT_SET`, `NOT_STARTS_WITH`, `NOT_IN`, and `STARTS_WITH`.
+
+Optional:
+
+- `value` (String)
+
+Read-Only:
+
+- `name` (String)
+
## Import
Import is supported using the following syntax:
diff --git a/examples/data-sources/sentry_issue_alert/data-source.tf b/examples/data-sources/sentry_issue_alert/data-source.tf
index e25553f13..e51578a73 100644
--- a/examples/data-sources/sentry_issue_alert/data-source.tf
+++ b/examples/data-sources/sentry_issue_alert/data-source.tf
@@ -1,25 +1,6 @@
# Retrieve an Issue Alert
-# URL format: https://sentry.io/organizations/[organization]/alerts/rules/[project]/[internal_id]/details/
data "sentry_issue_alert" "original" {
organization = "my-organization"
project = "my-project"
- internal_id = "42"
-}
-
-# Create a copy of an Issue Alert
-resource "sentry_issue_alert" "copy" {
- organization = data.sentry_issue_alert.original.organization
- project = data.sentry_issue_alert.original.project
-
- # Copy and modify attributes as necessary.
-
- name = "${data.sentry_issue_alert.original.name}-copy"
-
- action_match = data.sentry_issue_alert.original.action_match
- filter_match = data.sentry_issue_alert.original.filter_match
- frequency = data.sentry_issue_alert.original.frequency
-
- conditions = data.sentry_issue_alert.original.conditions
- filters = data.sentry_issue_alert.original.filters
- actions = data.sentry_issue_alert.original.actions
+ id = "42"
}
diff --git a/examples/resources/sentry_issue_alert/resource.tf b/examples/resources/sentry_issue_alert/resource.tf
index badb34511..9f50f60e8 100644
--- a/examples/resources/sentry_issue_alert/resource.tf
+++ b/examples/resources/sentry_issue_alert/resource.tf
@@ -8,81 +8,132 @@ resource "sentry_issue_alert" "main" {
filter_match = "any"
frequency = 30
- conditions = < for triage information"
- }
-]
-EOT
+ actions_v2 = [
+ {
+ slack_notify_service = {
+ workspace = data.sentry_organization_integration.slack.id
+ channel = "#warning"
+ tags = ["environment", "level"]
+ notes = "Please for triage information"
+ }
+ },
+ ]
// ...
}
@@ -179,6 +226,7 @@ EOT
# Send a Microsoft Teams notification
#
+# Retrieve a MS Teams integration
data "sentry_organization_integration" "msteams" {
organization = sentry_project.test.organization
@@ -186,16 +234,15 @@ data "sentry_organization_integration" "msteams" {
name = "My Team" # Name of your Microsoft Teams team
}
-resource "sentry_issue_alert" "msteams_alert" {
- actions = </
+ service = "my-service"
+ }
+ },
+ ]
// ...
}
#
-# Send a notification to a Sentry app with a custom webhook payload
+# Send a notification to a Sentry app
#
-resource "sentry_issue_alert" "notification_alert" {
- actions = < 0 {
- if conditions, err := json.Marshal(alert.Conditions); err == nil {
- m.Conditions = sentrytypes.NewLossyJsonValue(string(conditions))
- } else {
- return err
- }
- }
-
- m.Filters = sentrytypes.NewLossyJsonNull()
- if len(alert.Filters) > 0 {
- if filters, err := json.Marshal(alert.Filters); err == nil {
- m.Filters = sentrytypes.NewLossyJsonValue(string(filters))
- } else {
- return err
- }
- }
-
- m.Actions = sentrytypes.NewLossyJsonNull()
- if len(alert.Actions) > 0 {
- if actions, err := json.Marshal(alert.Actions); err == nil && len(actions) > 0 {
- m.Actions = sentrytypes.NewLossyJsonValue(string(actions))
- } else {
- return err
- }
- }
-
- frequency, err := alert.Frequency.Int64()
- if err != nil {
- return err
- }
- m.Frequency = types.Int64Value(frequency)
-
- m.Environment = types.StringPointerValue(alert.Environment)
- m.Owner = types.StringPointerValue(alert.Owner)
-
- return nil
-}
-
var _ datasource.DataSource = &IssueAlertDataSource{}
var _ datasource.DataSourceWithConfigure = &IssueAlertDataSource{}
@@ -92,6 +27,13 @@ func (d *IssueAlertDataSource) Metadata(ctx context.Context, req datasource.Meta
}
func (d *IssueAlertDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ stringAttribute := schema.StringAttribute{
+ Computed: true,
+ }
+ int64Attribute := schema.Int64Attribute{
+ Computed: true,
+ }
+
resp.Schema = schema.Schema{
MarkdownDescription: "Sentry Issue Alert data source. See the [Sentry documentation](https://docs.sentry.io/api/alerts/retrieve-an-issue-alert-rule-for-a-project/) for more information.",
@@ -111,16 +53,351 @@ func (d *IssueAlertDataSource) Schema(ctx context.Context, req datasource.Schema
Computed: true,
CustomType: sentrytypes.LossyJsonType{},
},
+ "conditions_v2": schema.ListNestedAttribute{
+ MarkdownDescription: "A list of triggers that determine when the rule fires.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "first_seen_event": schema.SingleNestedAttribute{
+ MarkdownDescription: "A new issue is created.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ },
+ },
+ "regression_event": schema.SingleNestedAttribute{
+ MarkdownDescription: "The issue changes state from resolved to unresolved.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ },
+ },
+ "reappeared_event": schema.SingleNestedAttribute{
+ MarkdownDescription: "The issue changes state from ignored to unresolved.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ },
+ },
+ "new_high_priority_issue": schema.SingleNestedAttribute{
+ MarkdownDescription: "Sentry marks a new issue as high priority.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ },
+ },
+ "existing_high_priority_issue": schema.SingleNestedAttribute{
+ MarkdownDescription: "Sentry marks an existing issue as high priority.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ },
+ },
+ "event_frequency": schema.SingleNestedAttribute{
+ MarkdownDescription: "When the `comparison_type` is `count`, the number of events in an issue is more than `value` in `interval`. When the `comparison_type` is `percent`, the number of events in an issue is `value` % higher in `interval` compared to `comparison_interval` ago.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "comparison_type": stringAttribute,
+ "comparison_interval": stringAttribute,
+ "value": int64Attribute,
+ "interval": stringAttribute,
+ },
+ },
+ "event_unique_user_frequency": schema.SingleNestedAttribute{
+ MarkdownDescription: "When the `comparison_type` is `count`, the number of users affected by an issue is more than `value` in `interval`. When the `comparison_type` is `percent`, the number of users affected by an issue is `value` % higher in `interval` compared to `comparison_interval` ago.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "comparison_type": stringAttribute,
+ "comparison_interval": stringAttribute,
+ "value": int64Attribute,
+ "interval": stringAttribute,
+ },
+ },
+ "event_frequency_percent": schema.SingleNestedAttribute{
+ MarkdownDescription: "When the `comparison_type` is `count`, the percent of sessions affected by an issue is more than `value` in `interval`. When the `comparison_type` is `percent`, the percent of sessions affected by an issue is `value` % higher in `interval` compared to `comparison_interval` ago.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "comparison_type": stringAttribute,
+ "comparison_interval": stringAttribute,
+ "value": schema.Float64Attribute{
+ Computed: true,
+ },
+ "interval": stringAttribute,
+ },
+ },
+ },
+ },
+ },
"filters": schema.StringAttribute{
MarkdownDescription: "A list of filters that determine if a rule fires after the necessary conditions have been met. In JSON string format.",
Computed: true,
CustomType: sentrytypes.LossyJsonType{},
},
+ "filters_v2": schema.ListNestedAttribute{
+ MarkdownDescription: "A list of filters that determine if a rule fires after the necessary conditions have been met.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "age_comparison": schema.SingleNestedAttribute{
+ MarkdownDescription: "The issue is older or newer than `value` `time`.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "comparison_type": stringAttribute,
+ "value": int64Attribute,
+ "time": stringAttribute,
+ },
+ },
+ "issue_occurrences": schema.SingleNestedAttribute{
+ MarkdownDescription: "The issue has happened at least `value` times (Note: this is approximate).",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "value": int64Attribute,
+ },
+ },
+ "assigned_to": schema.SingleNestedAttribute{
+ MarkdownDescription: "The issue is assigned to no one, team, or member.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "target_type": int64Attribute,
+ "target_identifier": int64Attribute,
+ },
+ },
+ "latest_adopted_release": schema.SingleNestedAttribute{
+ MarkdownDescription: "The {oldest_or_newest} adopted release associated with the event's issue is {older_or_newer} than the latest adopted release in {environment}.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "oldest_or_newest": int64Attribute,
+ "older_or_newer": int64Attribute,
+ "environment": int64Attribute,
+ },
+ },
+ "latest_release": schema.SingleNestedAttribute{
+ MarkdownDescription: "The event is from the latest release.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ },
+ },
+ "issue_category": schema.SingleNestedAttribute{
+ MarkdownDescription: "The issue's category is equal to `value`.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "value": stringAttribute,
+ },
+ },
+ "event_attribute": schema.SingleNestedAttribute{
+ MarkdownDescription: "The event's `attribute` value `match` `value`.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "attribute": stringAttribute,
+ "match": stringAttribute,
+ "value": stringAttribute,
+ },
+ },
+ "tagged_event": schema.SingleNestedAttribute{
+ MarkdownDescription: "The event's tags match `key` `match` `value`.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "key": stringAttribute,
+ "match": stringAttribute,
+ "value": stringAttribute,
+ },
+ },
+ "level": schema.SingleNestedAttribute{
+ MarkdownDescription: "The event's level is `match` `level`.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "match": stringAttribute,
+ "level": stringAttribute,
+ },
+ },
+ },
+ },
+ },
"actions": schema.StringAttribute{
MarkdownDescription: "List of actions. In JSON string format.",
Computed: true,
CustomType: sentrytypes.LossyJsonType{},
},
+ "actions_v2": schema.ListNestedAttribute{
+ MarkdownDescription: "A list of actions that take place when all required conditions and filters for the rule are met.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "notify_email": schema.SingleNestedAttribute{
+ MarkdownDescription: "Send a notification to `target_type` and if none can be found then send a notification to `fallthrough_type`.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "target_type": stringAttribute,
+ "target_identifier": stringAttribute,
+ "fallthrough_type": stringAttribute,
+ },
+ },
+ "notify_event": schema.SingleNestedAttribute{
+ MarkdownDescription: "Send a notification to all legacy integrations.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ },
+ },
+ "notify_event_service": schema.SingleNestedAttribute{
+ MarkdownDescription: "Send a notification via an integration.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "service": stringAttribute,
+ },
+ },
+ "notify_event_sentry_app": schema.SingleNestedAttribute{
+ MarkdownDescription: "Send a notification to a Sentry app.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "sentry_app_installation_uuid": stringAttribute,
+ "settings": schema.MapAttribute{
+ ElementType: types.StringType,
+ Computed: true,
+ },
+ },
+ },
+ "opsgenie_notify_team": schema.SingleNestedAttribute{
+ MarkdownDescription: "Send a notification to Opsgenie account `account` and team `team` with `priority` priority.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "account": stringAttribute,
+ "team": stringAttribute,
+ "priority": stringAttribute,
+ },
+ },
+ "pagerduty_notify_service": schema.SingleNestedAttribute{
+ MarkdownDescription: "Send a notification to PagerDuty account `account` and service `service` with `severity` severity.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "account": stringAttribute,
+ "service": stringAttribute,
+ "severity": stringAttribute,
+ },
+ },
+ "slack_notify_service": schema.SingleNestedAttribute{
+ MarkdownDescription: "Send a notification to the `workspace` Slack workspace to `channel` (optionally, an ID: `channel_id`) and show tags `tags` and notes `notes` in notification.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "workspace": stringAttribute,
+ "channel": stringAttribute,
+ "channel_id": stringAttribute,
+ "tags": schema.SetAttribute{
+ Computed: true,
+ CustomType: sentrytypes.StringSetType{
+ SetType: types.SetType{
+ ElemType: types.StringType,
+ },
+ },
+ },
+ "notes": stringAttribute,
+ },
+ },
+ "msteams_notify_service": schema.SingleNestedAttribute{
+ MarkdownDescription: "Send a notification to the `team` Team to `channel`.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "team": stringAttribute,
+ "channel": stringAttribute,
+ "channel_id": stringAttribute,
+ },
+ },
+ "discord_notify_service": schema.SingleNestedAttribute{
+ MarkdownDescription: "Send a notification to the `server` Discord server in the channel with ID or URL: `channel_id` and show tags `tags` in the notification.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "server": stringAttribute,
+ "channel_id": stringAttribute,
+ "tags": schema.SetAttribute{
+ Computed: true,
+ CustomType: sentrytypes.StringSetType{
+ SetType: types.SetType{
+ ElemType: types.StringType,
+ },
+ },
+ },
+ },
+ },
+ "jira_create_ticket": schema.SingleNestedAttribute{
+ MarkdownDescription: "Create a Jira issue in `integration`.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "integration": stringAttribute,
+ "project": stringAttribute,
+ "issue_type": stringAttribute,
+ },
+ },
+ "jira_server_create_ticket": schema.SingleNestedAttribute{
+ MarkdownDescription: "Create a Jira Server issue in `integration`.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "integration": stringAttribute,
+ "project": stringAttribute,
+ "issue_type": stringAttribute,
+ },
+ },
+ "github_create_ticket": schema.SingleNestedAttribute{
+ MarkdownDescription: "Create a GitHub issue in `integration`.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "integration": stringAttribute,
+ "repo": stringAttribute,
+ "assignee": stringAttribute,
+ "labels": schema.SetAttribute{
+ Computed: true,
+ ElementType: types.StringType,
+ },
+ },
+ },
+ "github_enterprise_create_ticket": schema.SingleNestedAttribute{
+ MarkdownDescription: "Create a GitHub Enterprise issue in `integration`.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "integration": stringAttribute,
+ "repo": stringAttribute,
+ "assignee": stringAttribute,
+ "labels": schema.SetAttribute{
+ Computed: true,
+ ElementType: types.StringType,
+ },
+ },
+ },
+ "azure_devops_create_ticket": schema.SingleNestedAttribute{
+ MarkdownDescription: "Create an Azure DevOps work item in `integration`.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "name": stringAttribute,
+ "integration": stringAttribute,
+ "work_item_type": stringAttribute,
+ },
+ },
+ },
+ },
+ },
"action_match": schema.StringAttribute{
MarkdownDescription: "Trigger actions when an event is captured by Sentry and `any` or `all` of the specified conditions happen.",
Computed: true,
@@ -146,31 +423,33 @@ func (d *IssueAlertDataSource) Schema(ctx context.Context, req datasource.Schema
}
func (d *IssueAlertDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
- var data IssueAlertResourceModel
+ var data IssueAlertModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
- action, apiResp, err := d.client.IssueAlerts.Get(
+ httpResp, err := d.apiClient.GetProjectRuleWithResponse(
ctx,
data.Organization.ValueString(),
data.Project.ValueString(),
data.Id.ValueString(),
)
- if apiResp.StatusCode == http.StatusNotFound {
+ if err != nil {
+ resp.Diagnostics.Append(diagutils.NewClientError("read", err))
+ return
+ } else if httpResp.StatusCode() == http.StatusNotFound {
resp.Diagnostics.Append(diagutils.NewNotFoundError("issue alert"))
resp.State.RemoveResource(ctx)
return
- }
- if err != nil {
- resp.Diagnostics.Append(diagutils.NewClientError("read", err))
+ } else if httpResp.StatusCode() != http.StatusOK || httpResp.JSON200 == nil {
+ resp.Diagnostics.Append(diagutils.NewClientStatusError("read", httpResp.StatusCode(), httpResp.Body))
return
}
- if err := data.Fill(data.Organization.ValueString(), *action); err != nil {
- resp.Diagnostics.Append(diagutils.NewFillError(err))
+ resp.Diagnostics.Append(data.Fill(ctx, *httpResp.JSON200)...)
+ if resp.Diagnostics.HasError() {
return
}
diff --git a/internal/provider/data_source_issue_alert_test.go b/internal/provider/data_source_issue_alert_test.go
index 51eba70bd..db88e7a36 100644
--- a/internal/provider/data_source_issue_alert_test.go
+++ b/internal/provider/data_source_issue_alert_test.go
@@ -1,47 +1,24 @@
package provider
import (
- "context"
"fmt"
"testing"
+ "github.com/hashicorp/terraform-plugin-testing/compare"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
- "github.com/hashicorp/terraform-plugin-testing/terraform"
+ "github.com/hashicorp/terraform-plugin-testing/knownvalue"
+ "github.com/hashicorp/terraform-plugin-testing/statecheck"
+ "github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
"github.com/jianyuan/terraform-provider-sentry/internal/acctest"
- "github.com/jianyuan/terraform-provider-sentry/internal/sentrytypes"
)
func TestAccIssueAlertDataSource(t *testing.T) {
rn := "sentry_issue_alert.test"
- rnCopy := "sentry_issue_alert.test_copy"
dsn := "data.sentry_issue_alert.test"
team := acctest.RandomWithPrefix("tf-team")
project := acctest.RandomWithPrefix("tf-project")
alert := acctest.RandomWithPrefix("tf-issue-alert")
var alertId string
- var alertIdCopy string
-
- checkResourceAttrJsonPair := func(a, b, attr string) resource.TestCheckFunc {
- return func(s *terraform.State) error {
- resA, ok := s.RootModule().Resources[a]
- if !ok {
- return fmt.Errorf("resource %s not found", a)
- }
-
- resB, ok := s.RootModule().Resources[b]
- if !ok {
- return fmt.Errorf("resource %s not found", b)
- }
-
- expected := sentrytypes.NewLossyJsonValue(resA.Primary.Attributes[attr])
- given := sentrytypes.NewLossyJsonValue(resB.Primary.Attributes[attr])
- match, diags := expected.StringSemanticEquals(context.Background(), given)
- if !match {
- return fmt.Errorf("expected %s, got %s: %s", expected, given, diags)
- }
- return nil
- }
- }
resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
@@ -51,30 +28,16 @@ func TestAccIssueAlertDataSource(t *testing.T) {
Config: testAccIssueAlertDataSourceConfig(team, project, alert),
Check: resource.ComposeTestCheckFunc(
testAccCheckIssueAlertExists(rn, &alertId),
- resource.TestCheckResourceAttr(dsn, "organization", acctest.TestOrganization),
- resource.TestCheckResourceAttr(dsn, "project", project),
- resource.TestCheckResourceAttrPair(dsn, "organization", rn, "organization"),
- resource.TestCheckResourceAttrPair(dsn, "project", rn, "project"),
- checkResourceAttrJsonPair(dsn, rn, "conditions"),
- checkResourceAttrJsonPair(dsn, rn, "filters"),
- checkResourceAttrJsonPair(dsn, rn, "actions"),
- resource.TestCheckResourceAttrPair(dsn, "action_match", rn, "action_match"),
- resource.TestCheckResourceAttrPair(dsn, "filter_match", rn, "filter_match"),
- resource.TestCheckResourceAttrPair(dsn, "frequency", rn, "frequency"),
- resource.TestCheckResourceAttrPair(dsn, "name", rn, "name"),
- resource.TestCheckResourceAttrPair(dsn, "environment", rn, "environment"),
- testAccCheckIssueAlertExists(rnCopy, &alertIdCopy),
- resource.TestCheckResourceAttrPair(rnCopy, "organization", rn, "organization"),
- resource.TestCheckResourceAttrPair(rnCopy, "project", rn, "project"),
- checkResourceAttrJsonPair(rnCopy, rn, "conditions"),
- checkResourceAttrJsonPair(rnCopy, rn, "filters"),
- checkResourceAttrJsonPair(rnCopy, rn, "actions"),
- resource.TestCheckResourceAttr(rnCopy, "action_match", "all"),
- resource.TestCheckResourceAttr(rnCopy, "filter_match", "all"),
- resource.TestCheckResourceAttrPair(rnCopy, "frequency", rn, "frequency"),
- resource.TestCheckResourceAttr(rnCopy, "name", alert+"-copy"),
- resource.TestCheckResourceAttrPair(rnCopy, "environment", rn, "environment"),
),
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue(dsn, tfjsonpath.New("organization"), knownvalue.StringExact(acctest.TestOrganization)),
+ statecheck.ExpectKnownValue(dsn, tfjsonpath.New("project"), knownvalue.StringExact(project)),
+ statecheck.CompareValuePairs(dsn, tfjsonpath.New("action_match"), rn, tfjsonpath.New("action_match"), compare.ValuesSame()),
+ statecheck.CompareValuePairs(dsn, tfjsonpath.New("filter_match"), rn, tfjsonpath.New("filter_match"), compare.ValuesSame()),
+ statecheck.CompareValuePairs(dsn, tfjsonpath.New("frequency"), rn, tfjsonpath.New("frequency"), compare.ValuesSame()),
+ statecheck.CompareValuePairs(dsn, tfjsonpath.New("name"), rn, tfjsonpath.New("name"), compare.ValuesSame()),
+ statecheck.CompareValuePairs(dsn, tfjsonpath.New("environment"), rn, tfjsonpath.New("environment"), compare.ValuesSame()),
+ },
},
},
})
@@ -197,19 +160,5 @@ data "sentry_issue_alert" "test" {
organization = sentry_issue_alert.test.organization
project = sentry_issue_alert.test.project
}
-
-resource "sentry_issue_alert" "test_copy" {
- organization = data.sentry_issue_alert.test.organization
- project = data.sentry_issue_alert.test.project
- name = "${data.sentry_issue_alert.test.name}-copy"
-
- action_match = "all"
- filter_match = "all"
- frequency = data.sentry_issue_alert.test.frequency
-
- conditions = data.sentry_issue_alert.test.conditions
- filters = data.sentry_issue_alert.test.filters
- actions = data.sentry_issue_alert.test.actions
-}
`, teamName, projectName, alertName)
}
diff --git a/internal/provider/model_issue_alert.go b/internal/provider/model_issue_alert.go
new file mode 100644
index 000000000..48563ca39
--- /dev/null
+++ b/internal/provider/model_issue_alert.go
@@ -0,0 +1,1482 @@
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/jianyuan/go-utils/ptr"
+ "github.com/jianyuan/go-utils/sliceutils"
+ "github.com/jianyuan/terraform-provider-sentry/internal/apiclient"
+ "github.com/jianyuan/terraform-provider-sentry/internal/sentrydata"
+ "github.com/jianyuan/terraform-provider-sentry/internal/sentrytypes"
+ "github.com/jianyuan/terraform-provider-sentry/internal/tfutils"
+)
+
+// Conditions
+
+type IssueAlertConditionFirstSeenEventModel struct {
+ Name types.String `tfsdk:"name"`
+}
+
+func (m *IssueAlertConditionFirstSeenEventModel) Fill(ctx context.Context, condition apiclient.ProjectRuleConditionFirstSeenEvent) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(condition.Name)
+ return
+}
+
+func (m IssueAlertConditionFirstSeenEventModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleCondition, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleCondition
+ err := v.FromProjectRuleConditionFirstSeenEvent(apiclient.ProjectRuleConditionFirstSeenEvent{
+ Name: m.Name.ValueStringPointer(),
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertConditionRegressionEventModel struct {
+ Name types.String `tfsdk:"name"`
+}
+
+func (m *IssueAlertConditionRegressionEventModel) Fill(ctx context.Context, condition apiclient.ProjectRuleConditionRegressionEvent) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(condition.Name)
+ return
+}
+
+func (m IssueAlertConditionRegressionEventModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleCondition, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleCondition
+ err := v.FromProjectRuleConditionRegressionEvent(apiclient.ProjectRuleConditionRegressionEvent{
+ Name: m.Name.ValueStringPointer(),
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertConditionReappearedEventModel struct {
+ Name types.String `tfsdk:"name"`
+}
+
+func (m *IssueAlertConditionReappearedEventModel) Fill(ctx context.Context, condition apiclient.ProjectRuleConditionReappearedEvent) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(condition.Name)
+ return
+}
+
+func (m IssueAlertConditionReappearedEventModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleCondition, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleCondition
+ err := v.FromProjectRuleConditionReappearedEvent(apiclient.ProjectRuleConditionReappearedEvent{
+ Name: m.Name.ValueStringPointer(),
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertConditionNewHighPriorityIssueModel struct {
+ Name types.String `tfsdk:"name"`
+}
+
+func (m *IssueAlertConditionNewHighPriorityIssueModel) Fill(ctx context.Context, condition apiclient.ProjectRuleConditionNewHighPriorityIssue) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(condition.Name)
+ return
+}
+
+func (m IssueAlertConditionNewHighPriorityIssueModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleCondition, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleCondition
+ err := v.FromProjectRuleConditionNewHighPriorityIssue(apiclient.ProjectRuleConditionNewHighPriorityIssue{
+ Name: m.Name.ValueStringPointer(),
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertConditionExistingHighPriorityIssueModel struct {
+ Name types.String `tfsdk:"name"`
+}
+
+func (m *IssueAlertConditionExistingHighPriorityIssueModel) Fill(ctx context.Context, condition apiclient.ProjectRuleConditionExistingHighPriorityIssue) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(condition.Name)
+ return
+}
+
+func (m IssueAlertConditionExistingHighPriorityIssueModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleCondition, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleCondition
+ err := v.FromProjectRuleConditionExistingHighPriorityIssue(apiclient.ProjectRuleConditionExistingHighPriorityIssue{
+ Name: m.Name.ValueStringPointer(),
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertConditionEventFrequencyModel struct {
+ Name types.String `tfsdk:"name"`
+ ComparisonType types.String `tfsdk:"comparison_type"`
+ ComparisonInterval types.String `tfsdk:"comparison_interval"`
+ Value types.Int64 `tfsdk:"value"`
+ Interval types.String `tfsdk:"interval"`
+}
+
+func (m *IssueAlertConditionEventFrequencyModel) Fill(ctx context.Context, condition apiclient.ProjectRuleConditionEventFrequency) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(condition.Name)
+ m.ComparisonType = types.StringValue(condition.ComparisonType)
+ m.ComparisonInterval = types.StringPointerValue(condition.ComparisonInterval)
+ m.Value = types.Int64Value(condition.Value)
+ m.Interval = types.StringValue(condition.Interval)
+ return
+}
+
+func (m IssueAlertConditionEventFrequencyModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleCondition, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleCondition
+ err := v.FromProjectRuleConditionEventFrequency(apiclient.ProjectRuleConditionEventFrequency{
+ Name: m.Name.ValueStringPointer(),
+ ComparisonType: m.ComparisonType.ValueString(),
+ ComparisonInterval: m.ComparisonInterval.ValueStringPointer(),
+ Value: m.Value.ValueInt64(),
+ Interval: m.Interval.ValueString(),
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertConditionEventUniqueUserFrequencyModel struct {
+ Name types.String `tfsdk:"name"`
+ ComparisonType types.String `tfsdk:"comparison_type"`
+ ComparisonInterval types.String `tfsdk:"comparison_interval"`
+ Value types.Int64 `tfsdk:"value"`
+ Interval types.String `tfsdk:"interval"`
+}
+
+func (m *IssueAlertConditionEventUniqueUserFrequencyModel) Fill(ctx context.Context, condition apiclient.ProjectRuleConditionEventUniqueUserFrequency) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(condition.Name)
+ m.ComparisonType = types.StringValue(condition.ComparisonType)
+ m.ComparisonInterval = types.StringPointerValue(condition.ComparisonInterval)
+ m.Value = types.Int64Value(condition.Value)
+ m.Interval = types.StringValue(condition.Interval)
+ return
+}
+
+func (m IssueAlertConditionEventUniqueUserFrequencyModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleCondition, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleCondition
+ err := v.FromProjectRuleConditionEventUniqueUserFrequency(apiclient.ProjectRuleConditionEventUniqueUserFrequency{
+ Name: m.Name.ValueStringPointer(),
+ ComparisonType: m.ComparisonType.ValueString(),
+ ComparisonInterval: m.ComparisonInterval.ValueStringPointer(),
+ Value: m.Value.ValueInt64(),
+ Interval: m.Interval.ValueString(),
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertConditionEventFrequencyPercentModel struct {
+ Name types.String `tfsdk:"name"`
+ ComparisonType types.String `tfsdk:"comparison_type"`
+ ComparisonInterval types.String `tfsdk:"comparison_interval"`
+ Value types.Float64 `tfsdk:"value"`
+ Interval types.String `tfsdk:"interval"`
+}
+
+func (m *IssueAlertConditionEventFrequencyPercentModel) Fill(ctx context.Context, condition apiclient.ProjectRuleConditionEventFrequencyPercent) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(condition.Name)
+ m.ComparisonType = types.StringValue(condition.ComparisonType)
+ m.ComparisonInterval = types.StringPointerValue(condition.ComparisonInterval)
+ m.Value = types.Float64Value(condition.Value)
+ m.Interval = types.StringValue(condition.Interval)
+ return
+}
+
+func (m IssueAlertConditionEventFrequencyPercentModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleCondition, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleCondition
+ err := v.FromProjectRuleConditionEventFrequencyPercent(apiclient.ProjectRuleConditionEventFrequencyPercent{
+ Name: m.Name.ValueStringPointer(),
+ ComparisonType: m.ComparisonType.ValueString(),
+ ComparisonInterval: m.ComparisonInterval.ValueStringPointer(),
+ Value: m.Value.ValueFloat64(),
+ Interval: m.Interval.ValueString(),
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertConditionModel struct {
+ FirstSeenEvent *IssueAlertConditionFirstSeenEventModel `tfsdk:"first_seen_event"`
+ RegressionEvent *IssueAlertConditionRegressionEventModel `tfsdk:"regression_event"`
+ ReappearedEvent *IssueAlertConditionReappearedEventModel `tfsdk:"reappeared_event"`
+ NewHighPriorityIssue *IssueAlertConditionNewHighPriorityIssueModel `tfsdk:"new_high_priority_issue"`
+ ExistingHighPriorityIssue *IssueAlertConditionExistingHighPriorityIssueModel `tfsdk:"existing_high_priority_issue"`
+ EventFrequency *IssueAlertConditionEventFrequencyModel `tfsdk:"event_frequency"`
+ EventUniqueUserFrequency *IssueAlertConditionEventUniqueUserFrequencyModel `tfsdk:"event_unique_user_frequency"`
+ EventFrequencyPercent *IssueAlertConditionEventFrequencyPercentModel `tfsdk:"event_frequency_percent"`
+}
+
+func (m IssueAlertConditionModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleCondition, diag.Diagnostics) {
+ if m.FirstSeenEvent != nil {
+ return m.FirstSeenEvent.ToApi(ctx)
+ } else if m.RegressionEvent != nil {
+ return m.RegressionEvent.ToApi(ctx)
+ } else if m.ReappearedEvent != nil {
+ return m.ReappearedEvent.ToApi(ctx)
+ } else if m.NewHighPriorityIssue != nil {
+ return m.NewHighPriorityIssue.ToApi(ctx)
+ } else if m.ExistingHighPriorityIssue != nil {
+ return m.ExistingHighPriorityIssue.ToApi(ctx)
+ } else if m.EventFrequency != nil {
+ return m.EventFrequency.ToApi(ctx)
+ } else if m.EventUniqueUserFrequency != nil {
+ return m.EventUniqueUserFrequency.ToApi(ctx)
+ } else if m.EventFrequencyPercent != nil {
+ return m.EventFrequencyPercent.ToApi(ctx)
+ } else {
+ var diags diag.Diagnostics
+ diags.AddError("Exactly one condition must be set", "Exactly one condition must be set")
+ return nil, diags
+ }
+}
+
+func (m *IssueAlertConditionModel) Fill(ctx context.Context, condition apiclient.ProjectRuleCondition) (diags diag.Diagnostics) {
+ conditionValue, err := condition.ValueByDiscriminator()
+ if err != nil {
+ diags.AddError("Invalid condition", err.Error())
+ return
+ }
+
+ m.FirstSeenEvent = nil
+ m.RegressionEvent = nil
+ m.ReappearedEvent = nil
+ m.NewHighPriorityIssue = nil
+ m.ExistingHighPriorityIssue = nil
+ m.EventFrequency = nil
+ m.EventUniqueUserFrequency = nil
+ m.EventFrequencyPercent = nil
+
+ switch conditionValue := conditionValue.(type) {
+ case apiclient.ProjectRuleConditionFirstSeenEvent:
+ m.FirstSeenEvent = &IssueAlertConditionFirstSeenEventModel{}
+ diags.Append(m.FirstSeenEvent.Fill(ctx, conditionValue)...)
+ case apiclient.ProjectRuleConditionRegressionEvent:
+ m.RegressionEvent = &IssueAlertConditionRegressionEventModel{}
+ diags.Append(m.RegressionEvent.Fill(ctx, conditionValue)...)
+ case apiclient.ProjectRuleConditionReappearedEvent:
+ m.ReappearedEvent = &IssueAlertConditionReappearedEventModel{}
+ diags.Append(m.ReappearedEvent.Fill(ctx, conditionValue)...)
+ case apiclient.ProjectRuleConditionNewHighPriorityIssue:
+ m.NewHighPriorityIssue = &IssueAlertConditionNewHighPriorityIssueModel{}
+ diags.Append(m.NewHighPriorityIssue.Fill(ctx, conditionValue)...)
+ case apiclient.ProjectRuleConditionExistingHighPriorityIssue:
+ m.ExistingHighPriorityIssue = &IssueAlertConditionExistingHighPriorityIssueModel{}
+ diags.Append(m.ExistingHighPriorityIssue.Fill(ctx, conditionValue)...)
+ case apiclient.ProjectRuleConditionEventFrequency:
+ m.EventFrequency = &IssueAlertConditionEventFrequencyModel{}
+ diags.Append(m.EventFrequency.Fill(ctx, conditionValue)...)
+ case apiclient.ProjectRuleConditionEventUniqueUserFrequency:
+ m.EventUniqueUserFrequency = &IssueAlertConditionEventUniqueUserFrequencyModel{}
+ diags.Append(m.EventUniqueUserFrequency.Fill(ctx, conditionValue)...)
+ case apiclient.ProjectRuleConditionEventFrequencyPercent:
+ m.EventFrequencyPercent = &IssueAlertConditionEventFrequencyPercentModel{}
+ diags.Append(m.EventFrequencyPercent.Fill(ctx, conditionValue)...)
+ default:
+ diags.AddError("Unsupported condition", fmt.Sprintf("Unsupported condition type %T", conditionValue))
+ }
+
+ return
+}
+
+// Filters
+
+type IssueAlertFilterAgeComparisonModel struct {
+ Name types.String `tfsdk:"name"`
+ ComparisonType types.String `tfsdk:"comparison_type"`
+ Value types.Int64 `tfsdk:"value"`
+ Time types.String `tfsdk:"time"`
+}
+
+func (m *IssueAlertFilterAgeComparisonModel) Fill(ctx context.Context, filter apiclient.ProjectRuleFilterAgeComparison) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(filter.Name)
+ m.ComparisonType = types.StringValue(filter.ComparisonType)
+ m.Value = types.Int64Value(filter.Value)
+ m.Time = types.StringValue(filter.Time)
+ return
+}
+
+func (m IssueAlertFilterAgeComparisonModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleFilter, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleFilter
+ err := v.FromProjectRuleFilterAgeComparison(apiclient.ProjectRuleFilterAgeComparison{
+ Name: m.Name.ValueStringPointer(),
+ ComparisonType: m.ComparisonType.ValueString(),
+ Value: m.Value.ValueInt64(),
+ Time: m.Time.ValueString(),
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertFilterIssueOccurrencesModel struct {
+ Name types.String `tfsdk:"name"`
+ Value types.Int64 `tfsdk:"value"`
+}
+
+func (m *IssueAlertFilterIssueOccurrencesModel) Fill(ctx context.Context, filter apiclient.ProjectRuleFilterIssueOccurrences) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(filter.Name)
+ m.Value = types.Int64Value(filter.Value)
+ return
+}
+
+func (m IssueAlertFilterIssueOccurrencesModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleFilter, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleFilter
+ err := v.FromProjectRuleFilterIssueOccurrences(apiclient.ProjectRuleFilterIssueOccurrences{
+ Name: m.Name.ValueStringPointer(),
+ Value: m.Value.ValueInt64(),
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertFilterAssignedToModel struct {
+ Name types.String `tfsdk:"name"`
+ TargetType types.String `tfsdk:"target_type"`
+ TargetIdentifier types.String `tfsdk:"target_identifier"`
+}
+
+func (m *IssueAlertFilterAssignedToModel) Fill(ctx context.Context, filter apiclient.ProjectRuleFilterAssignedTo) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(filter.Name)
+ m.TargetType = types.StringValue(filter.TargetType)
+
+ if filter.TargetIdentifier == nil {
+ m.TargetIdentifier = types.StringNull()
+ } else if v, err := filter.TargetIdentifier.AsProjectRuleFilterAssignedToTargetIdentifier0(); err == nil {
+ if v == "" {
+ m.TargetIdentifier = types.StringNull()
+ } else {
+ m.TargetIdentifier = types.StringValue(v)
+ }
+ } else if v, err := filter.TargetIdentifier.AsProjectRuleFilterAssignedToTargetIdentifier1(); err == nil {
+ m.TargetIdentifier = types.StringValue(v.String())
+ }
+
+ return
+}
+
+func (m IssueAlertFilterAssignedToModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleFilter, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ var targetIdentifier *apiclient.ProjectRuleFilterAssignedTo_TargetIdentifier
+
+ if !m.TargetIdentifier.IsNull() {
+ targetIdentifier = &apiclient.ProjectRuleFilterAssignedTo_TargetIdentifier{}
+ err := targetIdentifier.FromProjectRuleFilterAssignedToTargetIdentifier0(m.TargetIdentifier.ValueString())
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ }
+
+ var v apiclient.ProjectRuleFilter
+ err := v.FromProjectRuleFilterAssignedTo(apiclient.ProjectRuleFilterAssignedTo{
+ Name: m.Name.ValueStringPointer(),
+ TargetType: m.TargetType.ValueString(),
+ TargetIdentifier: targetIdentifier,
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertFilterLatestAdoptedReleaseModel struct {
+ Name types.String `tfsdk:"name"`
+ OldestOrNewest types.String `tfsdk:"oldest_or_newest"`
+ OlderOrNewer types.String `tfsdk:"older_or_newer"`
+ Environment types.String `tfsdk:"environment"`
+}
+
+func (m *IssueAlertFilterLatestAdoptedReleaseModel) Fill(ctx context.Context, filter apiclient.ProjectRuleFilterLatestAdoptedRelease) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(filter.Name)
+ m.OldestOrNewest = types.StringValue(filter.OldestOrNewest)
+ m.OlderOrNewer = types.StringValue(filter.OlderOrNewer)
+ m.Environment = types.StringValue(filter.Environment)
+ return
+}
+
+func (m IssueAlertFilterLatestAdoptedReleaseModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleFilter, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleFilter
+ err := v.FromProjectRuleFilterLatestAdoptedRelease(apiclient.ProjectRuleFilterLatestAdoptedRelease{
+ Name: m.Name.ValueStringPointer(),
+ OldestOrNewest: m.OldestOrNewest.ValueString(),
+ OlderOrNewer: m.OlderOrNewer.ValueString(),
+ Environment: m.Environment.ValueString(),
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertFilterLatestReleaseModel struct {
+ Name types.String `tfsdk:"name"`
+}
+
+func (m *IssueAlertFilterLatestReleaseModel) Fill(ctx context.Context, filter apiclient.ProjectRuleFilterLatestRelease) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(filter.Name)
+ return
+}
+
+func (m IssueAlertFilterLatestReleaseModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleFilter, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleFilter
+ err := v.FromProjectRuleFilterLatestRelease(apiclient.ProjectRuleFilterLatestRelease{
+ Name: m.Name.ValueStringPointer(),
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertFilterIssueCategoryModel struct {
+ Name types.String `tfsdk:"name"`
+ Value types.String `tfsdk:"value"`
+}
+
+func (m *IssueAlertFilterIssueCategoryModel) Fill(ctx context.Context, filter apiclient.ProjectRuleFilterIssueCategory) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(filter.Name)
+
+ value, ok := sentrydata.IssueGroupCategoryIdToName[filter.Value]
+ if !ok {
+ diags.AddError("Invalid issue category", fmt.Sprintf("Invalid issue category %q. Please report this to the provider developers.", filter.Value))
+ return
+ }
+ m.Value = types.StringValue(value)
+
+ return
+}
+
+func (m IssueAlertFilterIssueCategoryModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleFilter, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleFilter
+ err := v.FromProjectRuleFilterIssueCategory(apiclient.ProjectRuleFilterIssueCategory{
+ Name: m.Name.ValueStringPointer(),
+ Value: sentrydata.IssueGroupCategoryNameToId[m.Value.ValueString()],
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertFilterEventAttributeModel struct {
+ Name types.String `tfsdk:"name"`
+ Attribute types.String `tfsdk:"attribute"`
+ Match types.String `tfsdk:"match"`
+ Value types.String `tfsdk:"value"`
+}
+
+func (m *IssueAlertFilterEventAttributeModel) Fill(ctx context.Context, filter apiclient.ProjectRuleFilterEventAttribute) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(filter.Name)
+ m.Attribute = types.StringValue(filter.Attribute)
+
+ match, ok := sentrydata.MatchTypeIdToName[filter.Match]
+ if !ok {
+ diags.AddError("Invalid match type", fmt.Sprintf("Invalid match type %q. Please report this to the provider developers.", filter.Match))
+ return
+ }
+ m.Match = types.StringValue(match)
+
+ if filter.Value == nil || *filter.Value == "" {
+ m.Value = types.StringNull()
+ } else {
+ m.Value = types.StringValue(*filter.Value)
+ }
+ return
+}
+
+func (m IssueAlertFilterEventAttributeModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleFilter, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleFilter
+ err := v.FromProjectRuleFilterEventAttribute(apiclient.ProjectRuleFilterEventAttribute{
+ Name: m.Name.ValueStringPointer(),
+ Attribute: m.Attribute.ValueString(),
+ Match: sentrydata.MatchTypeNameToId[m.Match.ValueString()],
+ Value: m.Value.ValueStringPointer(),
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertFilterTaggedEventModel struct {
+ Name types.String `tfsdk:"name"`
+ Key types.String `tfsdk:"key"`
+ Match types.String `tfsdk:"match"`
+ Value types.String `tfsdk:"value"`
+}
+
+func (m *IssueAlertFilterTaggedEventModel) Fill(ctx context.Context, filter apiclient.ProjectRuleFilterTaggedEvent) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(filter.Name)
+ m.Key = types.StringValue(filter.Key)
+
+ match, ok := sentrydata.MatchTypeIdToName[filter.Match]
+ if !ok {
+ diags.AddError("Invalid match type", fmt.Sprintf("Invalid match type %q. Please report this to the provider developers.", filter.Match))
+ return
+ }
+ m.Match = types.StringValue(match)
+
+ if filter.Value == nil || *filter.Value == "" {
+ m.Value = types.StringNull()
+ } else {
+ m.Value = types.StringValue(*filter.Value)
+ }
+ return
+}
+
+func (m IssueAlertFilterTaggedEventModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleFilter, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleFilter
+ err := v.FromProjectRuleFilterTaggedEvent(apiclient.ProjectRuleFilterTaggedEvent{
+ Name: m.Name.ValueStringPointer(),
+ Key: m.Key.ValueString(),
+ Match: sentrydata.MatchTypeNameToId[m.Match.ValueString()],
+ Value: m.Value.ValueStringPointer(),
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertFilterLevelModel struct {
+ Name types.String `tfsdk:"name"`
+ Match types.String `tfsdk:"match"`
+ Level types.String `tfsdk:"level"`
+}
+
+func (m *IssueAlertFilterLevelModel) Fill(ctx context.Context, filter apiclient.ProjectRuleFilterLevel) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(filter.Name)
+
+ match, ok := sentrydata.MatchTypeIdToName[filter.Match]
+ if !ok {
+ diags.AddError("Invalid match type", fmt.Sprintf("Invalid match type %q. Please report this to the provider developers.", filter.Match))
+ return
+ }
+ m.Match = types.StringValue(match)
+
+ level, ok := sentrydata.LogLevelIdToName[filter.Level]
+ if !ok {
+ diags.AddError("Invalid level", fmt.Sprintf("Invalid level %q. Please report this to the provider developers.", filter.Level))
+ return
+ }
+ m.Level = types.StringValue(level)
+ return
+}
+
+func (m IssueAlertFilterLevelModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleFilter, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleFilter
+ err := v.FromProjectRuleFilterLevel(apiclient.ProjectRuleFilterLevel{
+ Name: m.Name.ValueStringPointer(),
+ Match: sentrydata.MatchTypeNameToId[m.Match.ValueString()],
+ Level: sentrydata.LogLevelNameToId[m.Level.ValueString()],
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertFilterModel struct {
+ AgeComparison *IssueAlertFilterAgeComparisonModel `tfsdk:"age_comparison"`
+ IssueOccurrences *IssueAlertFilterIssueOccurrencesModel `tfsdk:"issue_occurrences"`
+ AssignedTo *IssueAlertFilterAssignedToModel `tfsdk:"assigned_to"`
+ LatestAdoptedRelease *IssueAlertFilterLatestAdoptedReleaseModel `tfsdk:"latest_adopted_release"`
+ LatestRelease *IssueAlertFilterLatestReleaseModel `tfsdk:"latest_release"`
+ IssueCategory *IssueAlertFilterIssueCategoryModel `tfsdk:"issue_category"`
+ EventAttribute *IssueAlertFilterEventAttributeModel `tfsdk:"event_attribute"`
+ TaggedEvent *IssueAlertFilterTaggedEventModel `tfsdk:"tagged_event"`
+ Level *IssueAlertFilterLevelModel `tfsdk:"level"`
+}
+
+func (m IssueAlertFilterModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleFilter, diag.Diagnostics) {
+ if m.AgeComparison != nil {
+ return m.AgeComparison.ToApi(ctx)
+ } else if m.IssueOccurrences != nil {
+ return m.IssueOccurrences.ToApi(ctx)
+ } else if m.AssignedTo != nil {
+ return m.AssignedTo.ToApi(ctx)
+ } else if m.LatestAdoptedRelease != nil {
+ return m.LatestAdoptedRelease.ToApi(ctx)
+ } else if m.LatestRelease != nil {
+ return m.LatestRelease.ToApi(ctx)
+ } else if m.IssueCategory != nil {
+ return m.IssueCategory.ToApi(ctx)
+ } else if m.EventAttribute != nil {
+ return m.EventAttribute.ToApi(ctx)
+ } else if m.TaggedEvent != nil {
+ return m.TaggedEvent.ToApi(ctx)
+ } else if m.Level != nil {
+ return m.Level.ToApi(ctx)
+ } else {
+ var diags diag.Diagnostics
+ diags.AddError("Exactly one filter must be set", "Exactly one filter must be set")
+ return nil, diags
+ }
+}
+
+func (m *IssueAlertFilterModel) Fill(ctx context.Context, filter apiclient.ProjectRuleFilter) (diags diag.Diagnostics) {
+ filterValue, err := filter.ValueByDiscriminator()
+ if err != nil {
+ diags.AddError("Invalid filter", err.Error())
+ return
+ }
+
+ m.AgeComparison = nil
+ m.IssueOccurrences = nil
+ m.AssignedTo = nil
+ m.LatestAdoptedRelease = nil
+ m.LatestRelease = nil
+ m.IssueCategory = nil
+ m.EventAttribute = nil
+ m.TaggedEvent = nil
+ m.Level = nil
+
+ switch filterValue := filterValue.(type) {
+ case apiclient.ProjectRuleFilterAgeComparison:
+ m.AgeComparison = &IssueAlertFilterAgeComparisonModel{}
+ diags.Append(m.AgeComparison.Fill(ctx, filterValue)...)
+ case apiclient.ProjectRuleFilterIssueOccurrences:
+ m.IssueOccurrences = &IssueAlertFilterIssueOccurrencesModel{}
+ diags.Append(m.IssueOccurrences.Fill(ctx, filterValue)...)
+ case apiclient.ProjectRuleFilterAssignedTo:
+ m.AssignedTo = &IssueAlertFilterAssignedToModel{}
+ diags.Append(m.AssignedTo.Fill(ctx, filterValue)...)
+ case apiclient.ProjectRuleFilterLatestAdoptedRelease:
+ m.LatestAdoptedRelease = &IssueAlertFilterLatestAdoptedReleaseModel{}
+ diags.Append(m.LatestAdoptedRelease.Fill(ctx, filterValue)...)
+ case apiclient.ProjectRuleFilterLatestRelease:
+ m.LatestRelease = &IssueAlertFilterLatestReleaseModel{}
+ diags.Append(m.LatestRelease.Fill(ctx, filterValue)...)
+ case apiclient.ProjectRuleFilterIssueCategory:
+ m.IssueCategory = &IssueAlertFilterIssueCategoryModel{}
+ diags.Append(m.IssueCategory.Fill(ctx, filterValue)...)
+ case apiclient.ProjectRuleFilterEventAttribute:
+ m.EventAttribute = &IssueAlertFilterEventAttributeModel{}
+ diags.Append(m.EventAttribute.Fill(ctx, filterValue)...)
+ case apiclient.ProjectRuleFilterTaggedEvent:
+ m.TaggedEvent = &IssueAlertFilterTaggedEventModel{}
+ diags.Append(m.TaggedEvent.Fill(ctx, filterValue)...)
+ case apiclient.ProjectRuleFilterLevel:
+ m.Level = &IssueAlertFilterLevelModel{}
+ diags.Append(m.Level.Fill(ctx, filterValue)...)
+ default:
+ diags.AddError("Unsupported filter", fmt.Sprintf("Unsupported filter type %T", filterValue))
+ }
+
+ return
+}
+
+// Actions
+
+type IssueAlertActionNotifyEmailModel struct {
+ Name types.String `tfsdk:"name"`
+ TargetType types.String `tfsdk:"target_type"`
+ TargetIdentifier types.String `tfsdk:"target_identifier"`
+ FallthroughType types.String `tfsdk:"fallthrough_type"`
+}
+
+func (m *IssueAlertActionNotifyEmailModel) Fill(ctx context.Context, action apiclient.ProjectRuleActionNotifyEmail) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(action.Name)
+ m.TargetType = types.StringValue(action.TargetType)
+
+ if action.TargetIdentifier == nil {
+ m.TargetIdentifier = types.StringNull()
+ } else if v, err := action.TargetIdentifier.AsProjectRuleActionNotifyEmailTargetIdentifier0(); err == nil {
+ if v == "" {
+ m.TargetIdentifier = types.StringNull()
+ } else {
+ m.TargetIdentifier = types.StringValue(v)
+ }
+ } else if v, err := action.TargetIdentifier.AsProjectRuleActionNotifyEmailTargetIdentifier1(); err == nil {
+ m.TargetIdentifier = types.StringValue(v.String())
+ }
+
+ // Only set FallthroughType for IssueOwners
+ if action.TargetType == "IssueOwners" {
+ m.FallthroughType = types.StringPointerValue(action.FallthroughType)
+ } else {
+ m.FallthroughType = types.StringNull()
+ }
+
+ return
+}
+
+func (m IssueAlertActionNotifyEmailModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleAction, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var targetIdentifier *apiclient.ProjectRuleActionNotifyEmail_TargetIdentifier
+
+ if !m.TargetIdentifier.IsNull() {
+ targetIdentifier = &apiclient.ProjectRuleActionNotifyEmail_TargetIdentifier{}
+ err := targetIdentifier.FromProjectRuleActionNotifyEmailTargetIdentifier0(m.TargetIdentifier.ValueString())
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ }
+
+ var v apiclient.ProjectRuleAction
+ err := v.FromProjectRuleActionNotifyEmail(apiclient.ProjectRuleActionNotifyEmail{
+ Name: m.Name.ValueStringPointer(),
+ TargetType: m.TargetType.ValueString(),
+ TargetIdentifier: targetIdentifier,
+ FallthroughType: m.FallthroughType.ValueStringPointer(),
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertActionNotifyEventModel struct {
+ Name types.String `tfsdk:"name"`
+}
+
+func (m *IssueAlertActionNotifyEventModel) Fill(ctx context.Context, action apiclient.ProjectRuleActionNotifyEvent) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(action.Name)
+ return
+}
+
+func (m IssueAlertActionNotifyEventModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleAction, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleAction
+ err := v.FromProjectRuleActionNotifyEvent(apiclient.ProjectRuleActionNotifyEvent{
+ Name: m.Name.ValueStringPointer(),
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertActionNotifyEventServiceModel struct {
+ Name types.String `tfsdk:"name"`
+ Service types.String `tfsdk:"service"`
+}
+
+func (m *IssueAlertActionNotifyEventServiceModel) Fill(ctx context.Context, action apiclient.ProjectRuleActionNotifyEventService) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(action.Name)
+ m.Service = types.StringValue(action.Service)
+ return
+}
+
+func (m IssueAlertActionNotifyEventServiceModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleAction, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleAction
+ err := v.FromProjectRuleActionNotifyEventService(apiclient.ProjectRuleActionNotifyEventService{
+ Name: m.Name.ValueStringPointer(),
+ Service: m.Service.ValueString(),
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertActionNotifyEventSentryAppModel struct {
+ Name types.String `tfsdk:"name"`
+ SentryAppInstallationUuid types.String `tfsdk:"sentry_app_installation_uuid"`
+ Settings types.Map `tfsdk:"settings"`
+}
+
+func (m *IssueAlertActionNotifyEventSentryAppModel) Fill(ctx context.Context, action apiclient.ProjectRuleActionNotifyEventSentryApp) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(action.Name)
+ m.SentryAppInstallationUuid = types.StringValue(action.SentryAppInstallationUuid)
+
+ if action.Settings == nil {
+ m.Settings = types.MapNull(types.StringType)
+ } else {
+ var settingsMap = make(map[string]attr.Value)
+ for _, setting := range *action.Settings {
+ settingsMap[setting.Name] = types.StringValue(setting.Value)
+ }
+ m.Settings = types.MapValueMust(types.StringType, settingsMap)
+ }
+ return
+}
+
+func (m IssueAlertActionNotifyEventSentryAppModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleAction, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleAction
+
+ var settings *[]struct {
+ Name string `json:"name"`
+ Value string `json:"value"`
+ }
+
+ if !m.Settings.IsNull() {
+ elements := make(map[string]string, len(m.Settings.Elements()))
+ diags.Append(m.Settings.ElementsAs(ctx, &elements, false)...)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ settings = &[]struct {
+ Name string `json:"name"`
+ Value string `json:"value"`
+ }{}
+
+ for k, v := range elements {
+ *settings = append(*settings, struct {
+ Name string `json:"name"`
+ Value string `json:"value"`
+ }{
+ Name: k,
+ Value: v,
+ })
+ }
+ }
+
+ err := v.FromProjectRuleActionNotifyEventSentryApp(apiclient.ProjectRuleActionNotifyEventSentryApp{
+ Name: m.Name.ValueStringPointer(),
+ SentryAppInstallationUuid: m.SentryAppInstallationUuid.ValueString(),
+ Settings: settings,
+ HasSchemaFormConfig: true,
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertActionOpsgenieNotifyTeam struct {
+ Name types.String `tfsdk:"name"`
+ Account types.String `tfsdk:"account"`
+ Team types.String `tfsdk:"team"`
+ Priority types.String `tfsdk:"priority"`
+}
+
+func (m *IssueAlertActionOpsgenieNotifyTeam) Fill(ctx context.Context, action apiclient.ProjectRuleActionOpsgenieNotifyTeam) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(action.Name)
+ m.Account = types.StringValue(action.Account)
+ m.Team = types.StringValue(action.Team)
+ m.Priority = types.StringValue(action.Priority)
+ return
+}
+
+func (m IssueAlertActionOpsgenieNotifyTeam) ToApi(ctx context.Context) (*apiclient.ProjectRuleAction, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleAction
+ err := v.FromProjectRuleActionOpsgenieNotifyTeam(apiclient.ProjectRuleActionOpsgenieNotifyTeam{
+ Name: m.Name.ValueStringPointer(),
+ Account: m.Account.ValueString(),
+ Team: m.Team.ValueString(),
+ Priority: m.Priority.ValueString(),
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertActionPagerDutyNotifyServiceModel struct {
+ Name types.String `tfsdk:"name"`
+ Account types.String `tfsdk:"account"`
+ Service types.String `tfsdk:"service"`
+ Severity types.String `tfsdk:"severity"`
+}
+
+func (m *IssueAlertActionPagerDutyNotifyServiceModel) Fill(ctx context.Context, action apiclient.ProjectRuleActionPagerDutyNotifyService) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(action.Name)
+ m.Account = types.StringValue(action.Account)
+ m.Service = types.StringValue(action.Service)
+ m.Severity = types.StringValue(action.Severity)
+ return
+}
+
+func (m IssueAlertActionPagerDutyNotifyServiceModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleAction, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleAction
+ err := v.FromProjectRuleActionPagerDutyNotifyService(apiclient.ProjectRuleActionPagerDutyNotifyService{
+ Name: m.Name.ValueStringPointer(),
+ Account: m.Account.ValueString(),
+ Service: m.Service.ValueString(),
+ Severity: m.Severity.ValueString(),
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertActionSlackNotifyServiceModel struct {
+ Name types.String `tfsdk:"name"`
+ Workspace types.String `tfsdk:"workspace"`
+ Channel types.String `tfsdk:"channel"`
+ ChannelId types.String `tfsdk:"channel_id"`
+ Tags sentrytypes.StringSet `tfsdk:"tags"`
+ Notes types.String `tfsdk:"notes"`
+}
+
+func (m *IssueAlertActionSlackNotifyServiceModel) Fill(ctx context.Context, action apiclient.ProjectRuleActionSlackNotifyService) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(action.Name)
+ m.Workspace = types.StringValue(action.Workspace)
+ m.Channel = types.StringValue(action.Channel)
+ m.ChannelId = types.StringPointerValue(action.ChannelId)
+ m.Tags = tfutils.MergeDiagnostics(sentrytypes.StringSetPointerValue(action.Tags))(&diags)
+ m.Notes = types.StringPointerValue(action.Notes)
+ return
+}
+
+func (m IssueAlertActionSlackNotifyServiceModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleAction, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleAction
+ err := v.FromProjectRuleActionSlackNotifyService(apiclient.ProjectRuleActionSlackNotifyService{
+ Name: m.Name.ValueStringPointer(),
+ Workspace: m.Workspace.ValueString(),
+ Channel: m.Channel.ValueString(),
+ ChannelId: m.ChannelId.ValueStringPointer(),
+ Tags: tfutils.MergeDiagnostics(m.Tags.ValueStringPointer(ctx))(&diags),
+ Notes: m.Notes.ValueStringPointer(),
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertActionMsTeamsNotifyServiceModel struct {
+ Name types.String `tfsdk:"name"`
+ Team types.String `tfsdk:"team"`
+ Channel types.String `tfsdk:"channel"`
+ ChannelId types.String `tfsdk:"channel_id"`
+}
+
+func (m *IssueAlertActionMsTeamsNotifyServiceModel) Fill(ctx context.Context, action apiclient.ProjectRuleActionMsTeamsNotifyService) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(action.Name)
+ m.Team = types.StringValue(action.Team)
+ m.Channel = types.StringValue(action.Channel)
+ m.ChannelId = types.StringPointerValue(action.ChannelId)
+ return
+}
+
+func (m IssueAlertActionMsTeamsNotifyServiceModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleAction, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleAction
+ err := v.FromProjectRuleActionMsTeamsNotifyService(apiclient.ProjectRuleActionMsTeamsNotifyService{
+ Name: m.Name.ValueStringPointer(),
+ Team: m.Team.ValueString(),
+ Channel: m.Channel.ValueString(),
+ ChannelId: m.ChannelId.ValueStringPointer(),
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertActionDiscordNotifyServiceModel struct {
+ Name types.String `tfsdk:"name"`
+ Server types.String `tfsdk:"server"`
+ ChannelId types.String `tfsdk:"channel_id"`
+ Tags sentrytypes.StringSet `tfsdk:"tags"`
+}
+
+func (m *IssueAlertActionDiscordNotifyServiceModel) Fill(ctx context.Context, action apiclient.ProjectRuleActionDiscordNotifyService) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(action.Name)
+ m.Server = types.StringValue(action.Server)
+ m.ChannelId = types.StringValue(action.ChannelId)
+ m.Tags = tfutils.MergeDiagnostics(sentrytypes.StringSetPointerValue(action.Tags))(&diags)
+ return
+}
+
+func (m IssueAlertActionDiscordNotifyServiceModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleAction, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleAction
+ err := v.FromProjectRuleActionDiscordNotifyService(apiclient.ProjectRuleActionDiscordNotifyService{
+ Name: m.Name.ValueStringPointer(),
+ Server: m.Server.ValueString(),
+ ChannelId: m.ChannelId.ValueString(),
+ Tags: tfutils.MergeDiagnostics(m.Tags.ValueStringPointer(ctx))(&diags),
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertActionJiraCreateTicketModel struct {
+ Name types.String `tfsdk:"name"`
+ Integration types.String `tfsdk:"integration"`
+ Project types.String `tfsdk:"project"`
+ IssueType types.String `tfsdk:"issue_type"`
+}
+
+func (m *IssueAlertActionJiraCreateTicketModel) Fill(ctx context.Context, action apiclient.ProjectRuleActionJiraCreateTicket) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(action.Name)
+ m.Integration = types.StringValue(action.Integration)
+ m.Project = types.StringValue(action.Project)
+ m.IssueType = types.StringValue(action.IssueType)
+ return
+}
+
+func (m IssueAlertActionJiraCreateTicketModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleAction, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleAction
+ err := v.FromProjectRuleActionJiraCreateTicket(apiclient.ProjectRuleActionJiraCreateTicket{
+ Name: m.Name.ValueStringPointer(),
+ Integration: m.Integration.ValueString(),
+ Project: m.Project.ValueString(),
+ IssueType: m.IssueType.ValueString(),
+ DynamicFormFields: []map[string]interface{}{{"dummy": "dummy"}},
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertActionJiraServerCreateTicketModel struct {
+ Name types.String `tfsdk:"name"`
+ Integration types.String `tfsdk:"integration"`
+ Project types.String `tfsdk:"project"`
+ IssueType types.String `tfsdk:"issue_type"`
+}
+
+func (m *IssueAlertActionJiraServerCreateTicketModel) Fill(ctx context.Context, action apiclient.ProjectRuleActionJiraServerCreateTicket) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(action.Name)
+ m.Integration = types.StringValue(action.Integration)
+ m.Project = types.StringValue(action.Project)
+ m.IssueType = types.StringValue(action.IssueType)
+ return
+}
+
+func (m IssueAlertActionJiraServerCreateTicketModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleAction, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleAction
+ err := v.FromProjectRuleActionJiraServerCreateTicket(apiclient.ProjectRuleActionJiraServerCreateTicket{
+ Name: m.Name.ValueStringPointer(),
+ Integration: m.Integration.ValueString(),
+ Project: m.Project.ValueString(),
+ IssueType: m.IssueType.ValueString(),
+ DynamicFormFields: []map[string]interface{}{{"dummy": "dummy"}},
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertActionGitHubCreateTicketModel struct {
+ Name types.String `tfsdk:"name"`
+ Integration types.String `tfsdk:"integration"`
+ Repo types.String `tfsdk:"repo"`
+ Assignee types.String `tfsdk:"assignee"`
+ Labels types.Set `tfsdk:"labels"`
+}
+
+func (m *IssueAlertActionGitHubCreateTicketModel) Fill(ctx context.Context, action apiclient.ProjectRuleActionGitHubCreateTicket) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(action.Name)
+ m.Integration = types.StringValue(action.Integration)
+ m.Repo = types.StringValue(action.Repo)
+ m.Assignee = types.StringPointerValue(action.Assignee)
+
+ if action.Labels == nil {
+ m.Labels = types.SetNull(types.StringType)
+ } else {
+ m.Labels = types.SetValueMust(types.StringType, sliceutils.Map(func(v string) attr.Value {
+ return types.StringValue(v)
+ }, *action.Labels))
+ }
+ return
+}
+
+func (m IssueAlertActionGitHubCreateTicketModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleAction, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ body := apiclient.ProjectRuleActionGitHubCreateTicket{
+ Name: m.Name.ValueStringPointer(),
+ Integration: m.Integration.ValueString(),
+ Repo: m.Repo.ValueString(),
+ Assignee: m.Assignee.ValueStringPointer(),
+ DynamicFormFields: []map[string]interface{}{{"dummy": "dummy"}},
+ }
+
+ if !m.Labels.IsNull() {
+ var labels []string
+ diags.Append(m.Labels.ElementsAs(ctx, &labels, false)...)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ body.Labels = &labels
+ }
+
+ var v apiclient.ProjectRuleAction
+ err := v.FromProjectRuleActionGitHubCreateTicket(body)
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertActionGitHubEnterpriseCreateTicketModel struct {
+ Name types.String `tfsdk:"name"`
+ Integration types.String `tfsdk:"integration"`
+ Repo types.String `tfsdk:"repo"`
+ Assignee types.String `tfsdk:"assignee"`
+ Labels types.Set `tfsdk:"labels"`
+}
+
+func (m *IssueAlertActionGitHubEnterpriseCreateTicketModel) Fill(ctx context.Context, action apiclient.ProjectRuleActionGitHubEnterpriseCreateTicket) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(action.Name)
+ m.Integration = types.StringValue(action.Integration)
+ m.Repo = types.StringValue(action.Repo)
+ m.Assignee = types.StringPointerValue(action.Assignee)
+
+ if action.Labels == nil {
+ m.Labels = types.SetNull(types.StringType)
+ } else {
+ m.Labels = types.SetValueMust(types.StringType, sliceutils.Map(func(v string) attr.Value {
+ return types.StringValue(v)
+ }, *action.Labels))
+ }
+ return
+}
+
+func (m IssueAlertActionGitHubEnterpriseCreateTicketModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleAction, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ body := apiclient.ProjectRuleActionGitHubEnterpriseCreateTicket{
+ Name: m.Name.ValueStringPointer(),
+ Integration: m.Integration.ValueString(),
+ Repo: m.Repo.ValueString(),
+ Assignee: m.Assignee.ValueStringPointer(),
+ DynamicFormFields: []map[string]interface{}{{"dummy": "dummy"}},
+ }
+
+ if !m.Labels.IsNull() {
+ var labels []string
+ diags.Append(m.Labels.ElementsAs(ctx, &labels, false)...)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ body.Labels = &labels
+ }
+
+ var v apiclient.ProjectRuleAction
+ err := v.FromProjectRuleActionGitHubEnterpriseCreateTicket(body)
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertActionAzureDevopsCreateTicketModel struct {
+ Name types.String `tfsdk:"name"`
+ Integration types.String `tfsdk:"integration"`
+ Project types.String `tfsdk:"project"`
+ WorkItemType types.String `tfsdk:"work_item_type"`
+}
+
+func (m *IssueAlertActionAzureDevopsCreateTicketModel) Fill(ctx context.Context, action apiclient.ProjectRuleActionAzureDevopsCreateTicket) (diags diag.Diagnostics) {
+ m.Name = types.StringPointerValue(action.Name)
+ m.Integration = types.StringValue(action.Integration)
+ m.Project = types.StringValue(action.Project)
+ m.WorkItemType = types.StringValue(action.WorkItemType)
+ return
+}
+
+func (m IssueAlertActionAzureDevopsCreateTicketModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleAction, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var v apiclient.ProjectRuleAction
+ err := v.FromProjectRuleActionAzureDevopsCreateTicket(apiclient.ProjectRuleActionAzureDevopsCreateTicket{
+ Name: m.Name.ValueStringPointer(),
+ Integration: m.Integration.ValueString(),
+ Project: m.Project.ValueString(),
+ WorkItemType: m.WorkItemType.ValueString(),
+ DynamicFormFields: []map[string]interface{}{{"dummy": "dummy"}},
+ })
+ if err != nil {
+ diags.AddError("Failed to convert to API model", err.Error())
+ return nil, diags
+ }
+ return &v, diags
+}
+
+type IssueAlertActionModel struct {
+ NotifyEmail *IssueAlertActionNotifyEmailModel `tfsdk:"notify_email"`
+ NotifyEvent *IssueAlertActionNotifyEventModel `tfsdk:"notify_event"`
+ NotifyEventService *IssueAlertActionNotifyEventServiceModel `tfsdk:"notify_event_service"`
+ NotifyEventSentryApp *IssueAlertActionNotifyEventSentryAppModel `tfsdk:"notify_event_sentry_app"`
+ OpsgenieNotifyTeam *IssueAlertActionOpsgenieNotifyTeam `tfsdk:"opsgenie_notify_team"`
+ PagerDutyNotifyService *IssueAlertActionPagerDutyNotifyServiceModel `tfsdk:"pagerduty_notify_service"`
+ SlackNotifyService *IssueAlertActionSlackNotifyServiceModel `tfsdk:"slack_notify_service"`
+ MsTeamsNotifyService *IssueAlertActionMsTeamsNotifyServiceModel `tfsdk:"msteams_notify_service"`
+ DiscordNotifyService *IssueAlertActionDiscordNotifyServiceModel `tfsdk:"discord_notify_service"`
+ JiraCreateTicket *IssueAlertActionJiraCreateTicketModel `tfsdk:"jira_create_ticket"`
+ JiraServerCreateTicket *IssueAlertActionJiraServerCreateTicketModel `tfsdk:"jira_server_create_ticket"`
+ GitHubCreateTicket *IssueAlertActionGitHubCreateTicketModel `tfsdk:"github_create_ticket"`
+ GitHubEnterpriseCreateTicket *IssueAlertActionGitHubEnterpriseCreateTicketModel `tfsdk:"github_enterprise_create_ticket"`
+ AzureDevopsCreateTicket *IssueAlertActionAzureDevopsCreateTicketModel `tfsdk:"azure_devops_create_ticket"`
+}
+
+func (m IssueAlertActionModel) ToApi(ctx context.Context) (*apiclient.ProjectRuleAction, diag.Diagnostics) {
+ if m.NotifyEmail != nil {
+ return m.NotifyEmail.ToApi(ctx)
+ } else if m.NotifyEvent != nil {
+ return m.NotifyEvent.ToApi(ctx)
+ } else if m.NotifyEventService != nil {
+ return m.NotifyEventService.ToApi(ctx)
+ } else if m.NotifyEventSentryApp != nil {
+ return m.NotifyEventSentryApp.ToApi(ctx)
+ } else if m.OpsgenieNotifyTeam != nil {
+ return m.OpsgenieNotifyTeam.ToApi(ctx)
+ } else if m.PagerDutyNotifyService != nil {
+ return m.PagerDutyNotifyService.ToApi(ctx)
+ } else if m.SlackNotifyService != nil {
+ return m.SlackNotifyService.ToApi(ctx)
+ } else if m.MsTeamsNotifyService != nil {
+ return m.MsTeamsNotifyService.ToApi(ctx)
+ } else if m.DiscordNotifyService != nil {
+ return m.DiscordNotifyService.ToApi(ctx)
+ } else if m.JiraCreateTicket != nil {
+ return m.JiraCreateTicket.ToApi(ctx)
+ } else if m.JiraServerCreateTicket != nil {
+ return m.JiraServerCreateTicket.ToApi(ctx)
+ } else if m.GitHubCreateTicket != nil {
+ return m.GitHubCreateTicket.ToApi(ctx)
+ } else if m.GitHubEnterpriseCreateTicket != nil {
+ return m.GitHubEnterpriseCreateTicket.ToApi(ctx)
+ } else if m.AzureDevopsCreateTicket != nil {
+ return m.AzureDevopsCreateTicket.ToApi(ctx)
+ } else {
+ var diags diag.Diagnostics
+ diags.AddError("Exactly one action must be set", "Exactly one action must be set")
+ return nil, diags
+ }
+}
+
+func (m *IssueAlertActionModel) Fill(ctx context.Context, action apiclient.ProjectRuleAction) (diags diag.Diagnostics) {
+ actionValue, err := action.ValueByDiscriminator()
+ if err != nil {
+ diags.AddError("Invalid action", err.Error())
+ return
+ }
+
+ m.NotifyEmail = nil
+ m.NotifyEvent = nil
+ m.NotifyEventService = nil
+ m.NotifyEventSentryApp = nil
+ m.OpsgenieNotifyTeam = nil
+ m.PagerDutyNotifyService = nil
+ m.SlackNotifyService = nil
+ m.MsTeamsNotifyService = nil
+ m.DiscordNotifyService = nil
+ m.JiraCreateTicket = nil
+ m.JiraServerCreateTicket = nil
+ m.GitHubCreateTicket = nil
+ m.GitHubEnterpriseCreateTicket = nil
+ m.AzureDevopsCreateTicket = nil
+
+ switch actionValue := actionValue.(type) {
+ case apiclient.ProjectRuleActionNotifyEmail:
+ m.NotifyEmail = &IssueAlertActionNotifyEmailModel{}
+ diags.Append(m.NotifyEmail.Fill(ctx, actionValue)...)
+ case apiclient.ProjectRuleActionNotifyEvent:
+ m.NotifyEvent = &IssueAlertActionNotifyEventModel{}
+ diags.Append(m.NotifyEvent.Fill(ctx, actionValue)...)
+ case apiclient.ProjectRuleActionNotifyEventService:
+ m.NotifyEventService = &IssueAlertActionNotifyEventServiceModel{}
+ diags.Append(m.NotifyEventService.Fill(ctx, actionValue)...)
+ case apiclient.ProjectRuleActionNotifyEventSentryApp:
+ m.NotifyEventSentryApp = &IssueAlertActionNotifyEventSentryAppModel{}
+ diags.Append(m.NotifyEventSentryApp.Fill(ctx, actionValue)...)
+ case apiclient.ProjectRuleActionOpsgenieNotifyTeam:
+ m.OpsgenieNotifyTeam = &IssueAlertActionOpsgenieNotifyTeam{}
+ diags.Append(m.OpsgenieNotifyTeam.Fill(ctx, actionValue)...)
+ case apiclient.ProjectRuleActionPagerDutyNotifyService:
+ m.PagerDutyNotifyService = &IssueAlertActionPagerDutyNotifyServiceModel{}
+ diags.Append(m.PagerDutyNotifyService.Fill(ctx, actionValue)...)
+ case apiclient.ProjectRuleActionSlackNotifyService:
+ m.SlackNotifyService = &IssueAlertActionSlackNotifyServiceModel{}
+ diags.Append(m.SlackNotifyService.Fill(ctx, actionValue)...)
+ case apiclient.ProjectRuleActionMsTeamsNotifyService:
+ m.MsTeamsNotifyService = &IssueAlertActionMsTeamsNotifyServiceModel{}
+ diags.Append(m.MsTeamsNotifyService.Fill(ctx, actionValue)...)
+ case apiclient.ProjectRuleActionDiscordNotifyService:
+ m.DiscordNotifyService = &IssueAlertActionDiscordNotifyServiceModel{}
+ diags.Append(m.DiscordNotifyService.Fill(ctx, actionValue)...)
+ case apiclient.ProjectRuleActionJiraCreateTicket:
+ m.JiraCreateTicket = &IssueAlertActionJiraCreateTicketModel{}
+ diags.Append(m.JiraCreateTicket.Fill(ctx, actionValue)...)
+ case apiclient.ProjectRuleActionJiraServerCreateTicket:
+ m.JiraServerCreateTicket = &IssueAlertActionJiraServerCreateTicketModel{}
+ diags.Append(m.JiraServerCreateTicket.Fill(ctx, actionValue)...)
+ case apiclient.ProjectRuleActionGitHubCreateTicket:
+ m.GitHubCreateTicket = &IssueAlertActionGitHubCreateTicketModel{}
+ diags.Append(m.GitHubCreateTicket.Fill(ctx, actionValue)...)
+ case apiclient.ProjectRuleActionGitHubEnterpriseCreateTicket:
+ m.GitHubEnterpriseCreateTicket = &IssueAlertActionGitHubEnterpriseCreateTicketModel{}
+ diags.Append(m.GitHubEnterpriseCreateTicket.Fill(ctx, actionValue)...)
+ case apiclient.ProjectRuleActionAzureDevopsCreateTicket:
+ m.AzureDevopsCreateTicket = &IssueAlertActionAzureDevopsCreateTicketModel{}
+ diags.Append(m.AzureDevopsCreateTicket.Fill(ctx, actionValue)...)
+ default:
+ diags.AddError("Unsupported action", fmt.Sprintf("Unsupported action type %T", actionValue))
+ }
+
+ return
+}
+
+// Model
+
+type IssueAlertModel struct {
+ Id types.String `tfsdk:"id"`
+ Organization types.String `tfsdk:"organization"`
+ Project types.String `tfsdk:"project"`
+ Name types.String `tfsdk:"name"`
+ Conditions sentrytypes.LossyJson `tfsdk:"conditions"`
+ Filters sentrytypes.LossyJson `tfsdk:"filters"`
+ Actions sentrytypes.LossyJson `tfsdk:"actions"`
+ ActionMatch types.String `tfsdk:"action_match"`
+ FilterMatch types.String `tfsdk:"filter_match"`
+ Frequency types.Int64 `tfsdk:"frequency"`
+ Environment types.String `tfsdk:"environment"`
+ Owner types.String `tfsdk:"owner"`
+ ConditionsV2 *[]IssueAlertConditionModel `tfsdk:"conditions_v2"`
+ FiltersV2 *[]IssueAlertFilterModel `tfsdk:"filters_v2"`
+ ActionsV2 *[]IssueAlertActionModel `tfsdk:"actions_v2"`
+}
+
+func (m *IssueAlertModel) Fill(ctx context.Context, alert apiclient.ProjectRule) (diags diag.Diagnostics) {
+ m.Id = types.StringValue(alert.Id)
+
+ if len(alert.Projects) != 1 {
+ diags.AddError("Invalid project count", fmt.Sprintf("Expected 1 project, got %d", len(alert.Projects)))
+ return
+ }
+ m.Project = types.StringValue(alert.Projects[0])
+ m.Name = types.StringValue(alert.Name)
+ m.ActionMatch = types.StringValue(alert.ActionMatch)
+ m.FilterMatch = types.StringValue(alert.FilterMatch)
+ m.Frequency = types.Int64Value(alert.Frequency)
+ m.Environment = types.StringPointerValue(alert.Environment)
+ m.Owner = types.StringPointerValue(alert.Owner)
+
+ if !m.Conditions.IsNull() {
+ if conditions, err := json.Marshal(alert.Conditions); err == nil {
+ m.Conditions = sentrytypes.NewLossyJsonValue(string(conditions))
+ } else {
+ diags.AddError("Invalid conditions", err.Error())
+ return
+ }
+ } else if m.ConditionsV2 != nil {
+ m.ConditionsV2 = ptr.Ptr(sliceutils.Map(func(condition apiclient.ProjectRuleCondition) IssueAlertConditionModel {
+ var conditionModel IssueAlertConditionModel
+ diags.Append(conditionModel.Fill(ctx, condition)...)
+ return conditionModel
+ }, alert.Conditions))
+
+ if diags.HasError() {
+ return
+ }
+ }
+
+ if !m.Filters.IsNull() {
+ if filters, err := json.Marshal(alert.Filters); err == nil {
+ m.Filters = sentrytypes.NewLossyJsonValue(string(filters))
+ } else {
+ diags.AddError("Invalid filters", err.Error())
+ }
+ } else if m.FiltersV2 != nil {
+ m.FiltersV2 = ptr.Ptr(sliceutils.Map(func(filter apiclient.ProjectRuleFilter) IssueAlertFilterModel {
+ var filterModel IssueAlertFilterModel
+ diags.Append(filterModel.Fill(ctx, filter)...)
+ return filterModel
+ }, alert.Filters))
+
+ if diags.HasError() {
+ return
+ }
+ }
+
+ if !m.Actions.IsNull() {
+ if actions, err := json.Marshal(alert.Actions); err == nil && len(actions) > 0 {
+ m.Actions = sentrytypes.NewLossyJsonValue(string(actions))
+ } else {
+ diags.AddError("Invalid actions", err.Error())
+ }
+ } else if m.ActionsV2 != nil {
+ m.ActionsV2 = ptr.Ptr(sliceutils.Map(func(action apiclient.ProjectRuleAction) IssueAlertActionModel {
+ var actionModel IssueAlertActionModel
+ diags.Append(actionModel.Fill(ctx, action)...)
+ return actionModel
+ }, alert.Actions))
+
+ if diags.HasError() {
+ return
+ }
+ }
+
+ return
+}
diff --git a/internal/provider/resource_client_key.go b/internal/provider/resource_client_key.go
index b7fba89ba..369c2e7f7 100644
--- a/internal/provider/resource_client_key.go
+++ b/internal/provider/resource_client_key.go
@@ -80,10 +80,7 @@ func (m *ClientKeyResourceModel) Fill(ctx context.Context, key apiclient.Project
var javascriptLoaderScript ClientKeyJavascriptLoaderScriptResourceModel
diags.Append(javascriptLoaderScript.Fill(ctx, key)...)
-
- var javascriptLoaderScriptDiags diag.Diagnostics
- m.JavascriptLoaderScript, javascriptLoaderScriptDiags = types.ObjectValueFrom(ctx, javascriptLoaderScript.AttributeTypes(), javascriptLoaderScript)
- diags.Append(javascriptLoaderScriptDiags...)
+ m.JavascriptLoaderScript = tfutils.MergeDiagnostics(types.ObjectValueFrom(ctx, javascriptLoaderScript.AttributeTypes(), javascriptLoaderScript))(&diags)
m.Public = types.StringValue(key.Public)
m.Secret = types.StringValue(key.Secret)
diff --git a/internal/provider/resource_issue_alert.go b/internal/provider/resource_issue_alert.go
index 861aeec80..447908b5c 100644
--- a/internal/provider/resource_issue_alert.go
+++ b/internal/provider/resource_issue_alert.go
@@ -6,82 +6,25 @@ import (
"fmt"
"net/http"
+ "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
+ "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
- "github.com/jianyuan/go-sentry/v2/sentry"
"github.com/jianyuan/go-utils/must"
+ "github.com/jianyuan/terraform-provider-sentry/internal/apiclient"
"github.com/jianyuan/terraform-provider-sentry/internal/diagutils"
+ "github.com/jianyuan/terraform-provider-sentry/internal/sentrydata"
"github.com/jianyuan/terraform-provider-sentry/internal/sentrytypes"
+ "github.com/jianyuan/terraform-provider-sentry/internal/tfutils"
)
-type IssueAlertResourceModel struct {
- Id types.String `tfsdk:"id"`
- Organization types.String `tfsdk:"organization"`
- Project types.String `tfsdk:"project"`
- Name types.String `tfsdk:"name"`
- Conditions sentrytypes.LossyJson `tfsdk:"conditions"`
- Filters sentrytypes.LossyJson `tfsdk:"filters"`
- Actions sentrytypes.LossyJson `tfsdk:"actions"`
- ActionMatch types.String `tfsdk:"action_match"`
- FilterMatch types.String `tfsdk:"filter_match"`
- Frequency types.Int64 `tfsdk:"frequency"`
- Environment types.String `tfsdk:"environment"`
- Owner types.String `tfsdk:"owner"`
-}
-
-func (m *IssueAlertResourceModel) Fill(organization string, alert sentry.IssueAlert) error {
- m.Id = types.StringPointerValue(alert.ID)
- m.Organization = types.StringValue(organization)
- m.Project = types.StringValue(alert.Projects[0])
- m.Name = types.StringPointerValue(alert.Name)
- m.ActionMatch = types.StringPointerValue(alert.ActionMatch)
- m.FilterMatch = types.StringPointerValue(alert.FilterMatch)
- m.Owner = types.StringPointerValue(alert.Owner)
-
- m.Conditions = sentrytypes.NewLossyJsonValue("[]")
- if len(alert.Conditions) > 0 {
- if conditions, err := json.Marshal(alert.Conditions); err == nil {
- m.Conditions = sentrytypes.NewLossyJsonValue(string(conditions))
- } else {
- return err
- }
- }
-
- m.Filters = sentrytypes.NewLossyJsonNull()
- if len(alert.Filters) > 0 {
- if filters, err := json.Marshal(alert.Filters); err == nil {
- m.Filters = sentrytypes.NewLossyJsonValue(string(filters))
- } else {
- return err
- }
- }
-
- m.Actions = sentrytypes.NewLossyJsonNull()
- if len(alert.Actions) > 0 {
- if actions, err := json.Marshal(alert.Actions); err == nil && len(actions) > 0 {
- m.Actions = sentrytypes.NewLossyJsonValue(string(actions))
- } else {
- return err
- }
- }
-
- frequency, err := alert.Frequency.Int64()
- if err != nil {
- return err
- }
- m.Frequency = types.Int64Value(frequency)
-
- m.Environment = types.StringPointerValue(alert.Environment)
- m.Owner = types.StringPointerValue(alert.Owner)
-
- return nil
-}
-
var _ resource.Resource = &IssueAlertResource{}
+var _ resource.ResourceWithConfigValidators = &IssueAlertResource{}
+var _ resource.ResourceWithValidateConfig = &IssueAlertResource{}
var _ resource.ResourceWithConfigure = &IssueAlertResource{}
var _ resource.ResourceWithImportState = &IssueAlertResource{}
var _ resource.ResourceWithUpgradeState = &IssueAlertResource{}
@@ -99,14 +42,24 @@ func (r *IssueAlertResource) Metadata(ctx context.Context, req resource.Metadata
}
func (r *IssueAlertResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
- resp.Schema = schema.Schema{
- MarkdownDescription: `Create an Issue Alert Rule for a Project. See the [Sentry Documentation](https://docs.sentry.io/api/alerts/create-an-issue-alert-rule-for-a-project/) for more information.
+ nameStringAttribute := schema.StringAttribute{
+ Computed: true,
+ }
+ intervalStringAttribute := tfutils.WithEnumStringAttribute(schema.StringAttribute{
+ MarkdownDescription: "`m` for minutes, `h` for hours, `d` for days, and `w` for weeks.",
+ Optional: true,
+ }, []string{"1m", "5m", "15m", "1h", "1d", "1w", "30d"})
+ conditionComparisonTypeStringAttribute := tfutils.WithEnumStringAttribute(schema.StringAttribute{
+ Required: true,
+ }, []string{"count", "percent"})
+ conditionComparisonIntervalStringAttribute := tfutils.WithEnumStringAttribute(schema.StringAttribute{
+ MarkdownDescription: "`m` for minutes, `h` for hours, `d` for days, and `w` for weeks.",
+ Optional: true,
+ }, []string{"5m", "15m", "1h", "1d", "1w", "30d"})
-Please note the following changes since v0.12.0:
-- The attributes ` + "`conditions`" + `, ` + "`filters`" + `, and ` + "`actions`" + ` are in JSON string format. The types must match the Sentry API, otherwise Terraform will incorrectly detect a drift. Use ` + "`parseint(\"string\", 10)`" + ` to convert a string to an integer. Avoid using ` + "`jsonencode()`" + ` as it is unable to distinguish between an integer and a float.
-- The attribute ` + "`internal_id`" + ` has been removed. Use ` + "`id`" + ` instead.
-- The attribute ` + "`id`" + ` is now the ID of the issue alert. Previously, it was a combination of the organization, project, and issue alert ID.
- `,
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Create an Issue Alert Rule for a Project. See the [Sentry Documentation](https://docs.sentry.io/api/alerts/create-an-issue-alert-rule-for-a-project/) for more information.\n\n" +
+ "**NOTE:** Since v0.15.0, the `conditions`, `filters`, and `actions` attributes which are JSON strings have been deprecated in favor of `conditions_v2`, `filters_v2`, and `actions_v2` which are lists of objects.",
Version: 2,
@@ -122,36 +75,536 @@ Please note the following changes since v0.12.0:
},
},
"conditions": schema.StringAttribute{
- MarkdownDescription: "List of conditions. In JSON string format.",
- Required: true,
+ MarkdownDescription: "**Deprecated** in favor of `conditions_v2`. A list of triggers that determine when the rule fires. In JSON string format.",
+ DeprecationMessage: "Use `conditions_v2` instead.",
+ Optional: true,
CustomType: sentrytypes.LossyJsonType{
IgnoreKeys: []string{"name"},
},
+ Validators: []validator.String{
+ stringvalidator.ConflictsWith(path.MatchRoot("conditions_v2")),
+ },
+ },
+ "conditions_v2": schema.ListNestedAttribute{
+ MarkdownDescription: "A list of triggers that determine when the rule fires.",
+ Optional: true,
+ Validators: []validator.List{
+ listvalidator.ConflictsWith(path.MatchRoot("conditions")),
+ },
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: tfutils.WithMutuallyExclusiveValidator(map[string]schema.SingleNestedAttribute{
+ "first_seen_event": {
+ MarkdownDescription: "A new issue is created.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ },
+ },
+ "regression_event": {
+ MarkdownDescription: "The issue changes state from resolved to unresolved.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ },
+ },
+ "reappeared_event": {
+ MarkdownDescription: "The issue changes state from ignored to unresolved.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ },
+ },
+ "new_high_priority_issue": {
+ MarkdownDescription: "Sentry marks a new issue as high priority.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ },
+ },
+ "existing_high_priority_issue": {
+ MarkdownDescription: "Sentry marks an existing issue as high priority.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ },
+ },
+ "event_frequency": {
+ MarkdownDescription: "When the `comparison_type` is `count`, the number of events in an issue is more than `value` in `interval`. When the `comparison_type` is `percent`, the number of events in an issue is `value` % higher in `interval` compared to `comparison_interval` ago.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "comparison_type": conditionComparisonTypeStringAttribute,
+ "comparison_interval": conditionComparisonIntervalStringAttribute,
+ "value": schema.Int64Attribute{
+ Required: true,
+ },
+ "interval": intervalStringAttribute,
+ },
+ },
+ "event_unique_user_frequency": {
+ MarkdownDescription: "When the `comparison_type` is `count`, the number of users affected by an issue is more than `value` in `interval`. When the `comparison_type` is `percent`, the number of users affected by an issue is `value` % higher in `interval` compared to `comparison_interval` ago.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "comparison_type": conditionComparisonTypeStringAttribute,
+ "comparison_interval": conditionComparisonIntervalStringAttribute,
+ "value": schema.Int64Attribute{
+ Required: true,
+ },
+ "interval": intervalStringAttribute,
+ },
+ },
+ "event_frequency_percent": {
+ MarkdownDescription: "When the `comparison_type` is `count`, the percent of sessions affected by an issue is more than `value` in `interval`. When the `comparison_type` is `percent`, the percent of sessions affected by an issue is `value` % higher in `interval` compared to `comparison_interval` ago.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "comparison_type": conditionComparisonTypeStringAttribute,
+ "comparison_interval": conditionComparisonIntervalStringAttribute,
+ "value": schema.Float64Attribute{
+ Required: true,
+ },
+ "interval": tfutils.WithEnumStringAttribute(schema.StringAttribute{
+ MarkdownDescription: "`m` for minutes, `h` for hours.",
+ Required: true,
+ }, []string{"5m", "10m", "30m", "1h"}),
+ },
+ },
+ }),
+ },
},
"filters": schema.StringAttribute{
- MarkdownDescription: "A list of filters that determine if a rule fires after the necessary conditions have been met. In JSON string format.",
+ MarkdownDescription: "**Deprecated** in favor of `filters_v2`. A list of filters that determine if a rule fires after the necessary conditions have been met. In JSON string format.",
+ DeprecationMessage: "Use `filters_v2` instead.",
Optional: true,
CustomType: sentrytypes.LossyJsonType{},
+ Validators: []validator.String{
+ stringvalidator.ConflictsWith(path.MatchRoot("filters_v2")),
+ },
+ },
+ "filters_v2": schema.ListNestedAttribute{
+ MarkdownDescription: "A list of filters that determine if a rule fires after the necessary conditions have been met.",
+ Optional: true,
+ Validators: []validator.List{
+ listvalidator.ConflictsWith(path.MatchRoot("filters")),
+ },
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: tfutils.WithMutuallyExclusiveValidator(map[string]schema.SingleNestedAttribute{
+ "age_comparison": {
+ MarkdownDescription: "The issue is older or newer than `value` `time`.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "comparison_type": tfutils.WithEnumStringAttribute(schema.StringAttribute{
+ Required: true,
+ }, []string{"older", "newer"}),
+ "value": schema.Int64Attribute{
+ Required: true,
+ },
+ "time": tfutils.WithEnumStringAttribute(schema.StringAttribute{
+ Required: true,
+ }, []string{"minute", "hour", "day", "week"}),
+ },
+ },
+ "issue_occurrences": {
+ MarkdownDescription: "The issue has happened at least `value` times (Note: this is approximate).",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "value": schema.Int64Attribute{
+ Required: true,
+ },
+ },
+ },
+ "assigned_to": {
+ MarkdownDescription: "The issue is assigned to no one, team, or member.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "target_type": tfutils.WithEnumStringAttribute(schema.StringAttribute{
+ Required: true,
+ }, []string{"Unassigned", "Team", "Member"}),
+ "target_identifier": schema.StringAttribute{
+ MarkdownDescription: "The target's ID. Only required when `target_type` is `Team` or `Member`.",
+ Optional: true,
+ },
+ },
+ },
+ "latest_adopted_release": {
+ MarkdownDescription: "The {oldest_or_newest} adopted release associated with the event's issue is {older_or_newer} than the latest adopted release in {environment}.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "oldest_or_newest": tfutils.WithEnumStringAttribute(schema.StringAttribute{
+ Required: true,
+ }, []string{"oldest", "newest"}),
+ "older_or_newer": tfutils.WithEnumStringAttribute(schema.StringAttribute{
+ Required: true,
+ }, []string{"older", "newer"}),
+ "environment": schema.StringAttribute{
+ Required: true,
+ },
+ },
+ },
+ "latest_release": {
+ MarkdownDescription: "The event is from the latest release.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ },
+ },
+ "issue_category": {
+ MarkdownDescription: "The issue's category is equal to `value`.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "value": tfutils.WithEnumStringAttribute(schema.StringAttribute{
+ Required: true,
+ }, sentrydata.IssueGroupCategories),
+ },
+ },
+ "event_attribute": {
+ MarkdownDescription: "The event's `attribute` value `match` `value`.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "attribute": tfutils.WithEnumStringAttribute(schema.StringAttribute{
+ Required: true,
+ }, sentrydata.EventAttributes),
+ "match": tfutils.WithEnumStringAttribute(schema.StringAttribute{
+ MarkdownDescription: "The comparison operator.",
+ Required: true,
+ }, sentrydata.MatchTypes),
+ "value": schema.StringAttribute{
+ Optional: true,
+ },
+ },
+ },
+ "tagged_event": {
+ MarkdownDescription: "The event's tags match `key` `match` `value`.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "key": schema.StringAttribute{
+ MarkdownDescription: "The tag.",
+ Required: true,
+ },
+ "match": tfutils.WithEnumStringAttribute(schema.StringAttribute{
+ MarkdownDescription: "The comparison operator.",
+ Required: true,
+ }, sentrydata.MatchTypes),
+ "value": schema.StringAttribute{
+ Optional: true,
+ },
+ },
+ },
+ "level": {
+ MarkdownDescription: "The event's level is `match` `level`.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "match": tfutils.WithEnumStringAttribute(schema.StringAttribute{
+ MarkdownDescription: "The comparison operator.",
+ Required: true,
+ }, sentrydata.LevelMatchTypes),
+ "level": tfutils.WithEnumStringAttribute(schema.StringAttribute{
+ Required: true,
+ }, sentrydata.LogLevels),
+ },
+ },
+ }),
+ },
},
"actions": schema.StringAttribute{
- MarkdownDescription: "List of actions. In JSON string format.",
- Required: true,
+ MarkdownDescription: "**Deprecated** in favor of `actions_v2`. A list of actions that take place when all required conditions and filters for the rule are met. In JSON string format.",
+ DeprecationMessage: "Use `actions_v2` instead.",
+ Optional: true,
CustomType: sentrytypes.LossyJsonType{},
- },
- "action_match": schema.StringAttribute{
- MarkdownDescription: "Trigger actions when an event is captured by Sentry and `any` or `all` of the specified conditions happen.",
- Required: true,
Validators: []validator.String{
- stringvalidator.OneOf("all", "any"),
+ stringvalidator.ConflictsWith(path.MatchRoot("actions_v2")),
},
},
- "filter_match": schema.StringAttribute{
- MarkdownDescription: "A string determining which filters need to be true before any actions take place. Required when a value is provided for `filters`.",
+ "actions_v2": schema.ListNestedAttribute{
+ MarkdownDescription: "A list of actions that take place when all required conditions and filters for the rule are met.",
Optional: true,
- Validators: []validator.String{
- stringvalidator.OneOf("all", "any", "none"),
+ Validators: []validator.List{
+ listvalidator.ConflictsWith(path.MatchRoot("actions")),
+ listvalidator.SizeAtLeast(1),
+ },
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: tfutils.WithMutuallyExclusiveValidator(map[string]schema.SingleNestedAttribute{
+ "notify_email": {
+ MarkdownDescription: "Send a notification to `target_type` and if none can be found then send a notification to `fallthrough_type`.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "target_type": tfutils.WithEnumStringAttribute(schema.StringAttribute{
+ Required: true,
+ }, []string{"IssueOwners", "Team", "Member"}),
+ "target_identifier": schema.StringAttribute{
+ MarkdownDescription: "The ID of the Member or Team the notification should be sent to. Only required when `target_type` is `Team` or `Member`.",
+ Optional: true,
+ },
+ "fallthrough_type": tfutils.WithEnumStringAttribute(schema.StringAttribute{
+ MarkdownDescription: "Who the notification should be sent to if there are no suggested assignees.",
+ Optional: true,
+ }, []string{"AllMembers", "ActiveMembers", "NoOne"}),
+ },
+ },
+ "notify_event": {
+ MarkdownDescription: "Send a notification to all legacy integrations.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ },
+ },
+ "notify_event_service": {
+ MarkdownDescription: "Send a notification via an integration.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "service": schema.StringAttribute{
+ MarkdownDescription: "The slug of the integration service. Sourced from `https://terraform-provider-sentry.sentry.io/settings/developer-settings//`.",
+ Required: true,
+ },
+ },
+ },
+ "notify_event_sentry_app": {
+ MarkdownDescription: "Send a notification to a Sentry app.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "sentry_app_installation_uuid": schema.StringAttribute{
+ Required: true,
+ },
+ "settings": schema.MapAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ },
+ },
+ },
+ "opsgenie_notify_team": {
+ MarkdownDescription: "Send a notification to Opsgenie account `account` and team `team` with `priority` priority.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "account": schema.StringAttribute{
+ Required: true,
+ },
+ "team": schema.StringAttribute{
+ Required: true,
+ },
+ "priority": schema.StringAttribute{
+ Required: true,
+ },
+ },
+ },
+ "pagerduty_notify_service": {
+ MarkdownDescription: "Send a notification to PagerDuty account `account` and service `service` with `severity` severity.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "account": schema.StringAttribute{
+ Required: true,
+ },
+ "service": schema.StringAttribute{
+ Required: true,
+ },
+ "severity": schema.StringAttribute{
+ Required: true,
+ },
+ },
+ },
+ "slack_notify_service": {
+ MarkdownDescription: "Send a notification to the `workspace` Slack workspace to `channel` (optionally, an ID: `channel_id`) and show tags `tags` and notes `notes` in notification.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "workspace": schema.StringAttribute{
+ MarkdownDescription: "The integration ID associated with the Slack workspace.",
+ Required: true,
+ },
+ "channel": schema.StringAttribute{
+ MarkdownDescription: "The name of the channel to send the notification to (e.g., #critical, Jane Schmidt).",
+ Required: true,
+ },
+ "channel_id": schema.StringAttribute{
+ MarkdownDescription: "The ID of the channel to send the notification to.",
+ Computed: true,
+ },
+ "tags": schema.SetAttribute{
+ MarkdownDescription: "A string of tags to show in the notification.",
+ Optional: true,
+ CustomType: sentrytypes.StringSetType{
+ SetType: types.SetType{
+ ElemType: types.StringType,
+ },
+ },
+ },
+ "notes": schema.StringAttribute{
+ MarkdownDescription: "Text to show alongside the notification. To @ a user, include their user id like `@`. To include a clickable link, format the link and title like ``.",
+ Optional: true,
+ },
+ },
+ },
+ "msteams_notify_service": {
+ MarkdownDescription: "Send a notification to the `team` Team to `channel`.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "team": schema.StringAttribute{
+ MarkdownDescription: "The integration ID associated with the Microsoft Teams team.",
+ Required: true,
+ },
+ "channel": schema.StringAttribute{
+ MarkdownDescription: "The name of the channel to send the notification to.",
+ Required: true,
+ },
+ "channel_id": schema.StringAttribute{
+ Computed: true,
+ },
+ },
+ },
+ "discord_notify_service": {
+ MarkdownDescription: "Send a notification to the `server` Discord server in the channel with ID or URL: `channel_id` and show tags `tags` in the notification.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "server": schema.StringAttribute{
+ MarkdownDescription: "The integration ID associated with the Discord server.",
+ Required: true,
+ },
+ "channel_id": schema.StringAttribute{
+ MarkdownDescription: "The ID of the channel to send the notification to. You must enter either a channel ID or a channel URL, not a channel name",
+ Required: true,
+ },
+ "tags": schema.SetAttribute{
+ MarkdownDescription: "A string of tags to show in the notification.",
+ Optional: true,
+ CustomType: sentrytypes.StringSetType{
+ SetType: types.SetType{
+ ElemType: types.StringType,
+ },
+ },
+ },
+ },
+ },
+ "jira_create_ticket": {
+ MarkdownDescription: "Create a Jira issue in `integration`.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "integration": schema.StringAttribute{
+ MarkdownDescription: "The integration ID associated with Jira.",
+ Required: true,
+ },
+ "project": schema.StringAttribute{
+ MarkdownDescription: "The ID of the Jira project.",
+ Required: true,
+ },
+ "issue_type": schema.StringAttribute{
+ MarkdownDescription: "The ID of the type of issue that the ticket should be created as.",
+ Required: true,
+ },
+ },
+ },
+ "jira_server_create_ticket": {
+ MarkdownDescription: "Create a Jira Server issue in `integration`.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "integration": schema.StringAttribute{
+ MarkdownDescription: "The integration ID associated with Jira Server.",
+ Required: true,
+ },
+ "project": schema.StringAttribute{
+ MarkdownDescription: "The ID of the Jira Server project.",
+ Required: true,
+ },
+ "issue_type": schema.StringAttribute{
+ MarkdownDescription: "The ID of the type of issue that the ticket should be created as.",
+ Required: true,
+ },
+ },
+ },
+ "github_create_ticket": {
+ MarkdownDescription: "Create a GitHub issue in `integration`.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "integration": schema.StringAttribute{
+ MarkdownDescription: "The integration ID associated with GitHub.",
+ Required: true,
+ },
+ "repo": schema.StringAttribute{
+ MarkdownDescription: "The name of the repository to create the issue in.",
+ Required: true,
+ },
+ "assignee": schema.StringAttribute{
+ MarkdownDescription: "The GitHub user to assign the issue to.",
+ Optional: true,
+ },
+ "labels": schema.SetAttribute{
+ MarkdownDescription: "A list of labels to assign to the issue.",
+ Optional: true,
+ ElementType: types.StringType,
+ },
+ },
+ },
+ "github_enterprise_create_ticket": {
+ MarkdownDescription: "Create a GitHub Enterprise issue in `integration`.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "integration": schema.StringAttribute{
+ MarkdownDescription: "The integration ID associated with GitHub Enterprise.",
+ Required: true,
+ },
+ "repo": schema.StringAttribute{
+ MarkdownDescription: "The name of the repository to create the issue in.",
+ Required: true,
+ },
+ "assignee": schema.StringAttribute{
+ MarkdownDescription: "The GitHub user to assign the issue to.",
+ Optional: true,
+ },
+ "labels": schema.SetAttribute{
+ MarkdownDescription: "A list of labels to assign to the issue.",
+ Optional: true,
+ ElementType: types.StringType,
+ },
+ },
+ },
+ "azure_devops_create_ticket": {
+ MarkdownDescription: "Create an Azure DevOps work item in `integration`.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": nameStringAttribute,
+ "integration": schema.StringAttribute{
+ MarkdownDescription: "The integration ID.",
+ Required: true,
+ },
+ "project": schema.StringAttribute{
+ MarkdownDescription: "The ID of the Azure DevOps project.",
+ Required: true,
+ },
+ "work_item_type": schema.StringAttribute{
+ MarkdownDescription: "The type of work item to create.",
+ Required: true,
+ },
+ },
+ },
+ }),
},
},
+ "action_match": tfutils.WithEnumStringAttribute(schema.StringAttribute{
+ MarkdownDescription: "Trigger actions when an event is captured by Sentry and `any` or `all` of the specified conditions happen.",
+ Required: true,
+ }, []string{"all", "any"}),
+ "filter_match": tfutils.WithEnumStringAttribute(schema.StringAttribute{
+ MarkdownDescription: "A string determining which filters need to be true before any actions take place. Required when a value is provided for `filters`.",
+ Optional: true,
+ }, []string{"all", "any", "none"}),
"frequency": schema.Int64Attribute{
MarkdownDescription: "Perform actions at most once every `X` minutes for this issue.",
Required: true,
@@ -168,53 +621,173 @@ Please note the following changes since v0.12.0:
}
}
+func (r *IssueAlertResource) ConfigValidators(ctx context.Context) []resource.ConfigValidator {
+ return []resource.ConfigValidator{
+ resourcevalidator.AtLeastOneOf(
+ path.MatchRoot("actions"),
+ path.MatchRoot("actions_v2"),
+ ),
+ }
+}
+
+func (r *IssueAlertResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
+ var data IssueAlertModel
+
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if data.ConditionsV2 != nil {
+ for i, item := range *data.ConditionsV2 {
+ if _, diags := item.ToApi(ctx); diags.HasError() {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("conditions_v2").AtListIndex(i),
+ "Missing attribute configuration",
+ fmt.Sprintf("Failed to convert condition: %s", diags),
+ )
+ }
+ }
+ }
+
+ if data.FiltersV2 != nil {
+ for i, item := range *data.FiltersV2 {
+ if _, diags := item.ToApi(ctx); diags.HasError() {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("filters_v2").AtListIndex(i),
+ "Missing attribute configuration",
+ fmt.Sprintf("Failed to convert filter: %s", diags),
+ )
+ }
+ }
+ }
+
+ if !data.Actions.IsNull() {
+ if ok, _ := data.Actions.StringSemanticEquals(ctx, sentrytypes.NewLossyJsonValue(`[]`)); ok {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("actions"),
+ "Missing attribute configuration",
+ "You must add an action for this alert to fire",
+ )
+ }
+ } else if data.ActionsV2 != nil {
+ if len(*data.ActionsV2) == 0 {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("actions_v2"),
+ "Missing attribute configuration",
+ "You must add an action for this alert to fire",
+ )
+ }
+
+ for i, item := range *data.ActionsV2 {
+ if _, diags := item.ToApi(ctx); diags.HasError() {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("actions_v2").AtListIndex(i),
+ "Missing attribute configuration",
+ fmt.Sprintf("Failed to convert action: %s", diags),
+ )
+ }
+ }
+ }
+}
+
func (r *IssueAlertResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
- var data IssueAlertResourceModel
+ var data IssueAlertModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
- params := &sentry.IssueAlert{
- Name: data.Name.ValueStringPointer(),
- ActionMatch: data.ActionMatch.ValueStringPointer(),
- FilterMatch: data.FilterMatch.ValueStringPointer(),
- Frequency: sentry.JsonNumber(json.Number(data.Frequency.String())),
+ body := apiclient.CreateProjectRuleJSONRequestBody{
+ Name: data.Name.ValueString(),
+ ActionMatch: data.ActionMatch.ValueString(),
+ FilterMatch: data.FilterMatch.ValueString(),
+ Frequency: data.Frequency.ValueInt64(),
Owner: data.Owner.ValueStringPointer(),
Environment: data.Environment.ValueStringPointer(),
- Projects: []string{data.Project.String()},
+ Projects: []string{data.Project.ValueString()},
}
+
if !data.Conditions.IsNull() {
- resp.Diagnostics.Append(data.Conditions.Unmarshal(¶ms.Conditions)...)
+ resp.Diagnostics.Append(data.Conditions.Unmarshal(&body.Conditions)...)
+ } else if data.ConditionsV2 != nil {
+ body.Conditions = []apiclient.ProjectRuleCondition{}
+ for i, item := range *data.ConditionsV2 {
+ condition, diags := item.ToApi(ctx)
+ if diags.HasError() {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("conditions_v2").AtListIndex(i),
+ "Missing attribute configuration",
+ fmt.Sprintf("Failed to convert condition: %s", diags),
+ )
+ return
+ }
+ body.Conditions = append(body.Conditions, *condition)
+ }
+ } else {
+ body.Conditions = []apiclient.ProjectRuleCondition{}
}
+
if !data.Filters.IsNull() {
- resp.Diagnostics.Append(data.Filters.Unmarshal(¶ms.Filters)...)
+ resp.Diagnostics.Append(data.Filters.Unmarshal(&body.Filters)...)
+ } else if data.FiltersV2 != nil {
+ body.Filters = []apiclient.ProjectRuleFilter{}
+ for i, item := range *data.FiltersV2 {
+ filter, diags := item.ToApi(ctx)
+ if diags.HasError() {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("filters_v2").AtListIndex(i),
+ "Missing attribute configuration",
+ fmt.Sprintf("Failed to convert filter: %s", diags),
+ )
+ return
+ }
+ body.Filters = append(body.Filters, *filter)
+ }
+ } else {
+ body.Filters = []apiclient.ProjectRuleFilter{}
}
+
if !data.Actions.IsNull() {
- resp.Diagnostics.Append(data.Actions.Unmarshal(¶ms.Actions)...)
+ resp.Diagnostics.Append(data.Actions.Unmarshal(&body.Actions)...)
+ } else if data.ActionsV2 != nil {
+ body.Actions = []apiclient.ProjectRuleAction{}
+ for i, item := range *data.ActionsV2 {
+ action, diags := item.ToApi(ctx)
+ if diags.HasError() {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("actions_v2").AtListIndex(i),
+ "Missing attribute configuration",
+ fmt.Sprintf("Failed to convert action: %s", diags),
+ )
+ return
+ }
+ body.Actions = append(body.Actions, *action)
+ }
+ } else {
+ body.Actions = []apiclient.ProjectRuleAction{}
}
if resp.Diagnostics.HasError() {
return
}
- action, _, err := r.client.IssueAlerts.Create(
+ httpResp, err := r.apiClient.CreateProjectRuleWithResponse(
ctx,
data.Organization.ValueString(),
data.Project.ValueString(),
- params,
+ body,
)
if err != nil {
resp.Diagnostics.Append(diagutils.NewClientError("create", err))
return
- }
-
- if err := data.Fill(data.Organization.ValueString(), *action); err != nil {
- resp.Diagnostics.Append(diagutils.NewFillError(err))
+ } else if httpResp.StatusCode() != http.StatusOK || httpResp.JSON200 == nil {
+ resp.Diagnostics.Append(diagutils.NewClientStatusError("create", httpResp.StatusCode(), httpResp.Body))
return
}
+ resp.Diagnostics.Append(data.Fill(ctx, *httpResp.JSON200)...)
if resp.Diagnostics.HasError() {
return
}
@@ -223,31 +796,33 @@ func (r *IssueAlertResource) Create(ctx context.Context, req resource.CreateRequ
}
func (r *IssueAlertResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
- var data IssueAlertResourceModel
+ var data IssueAlertModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
- action, apiResp, err := r.client.IssueAlerts.Get(
+ httpResp, err := r.apiClient.GetProjectRuleWithResponse(
ctx,
data.Organization.ValueString(),
data.Project.ValueString(),
data.Id.ValueString(),
)
- if apiResp.StatusCode == http.StatusNotFound {
+ if err != nil {
+ resp.Diagnostics.Append(diagutils.NewClientError("read", err))
+ return
+ } else if httpResp.StatusCode() == http.StatusNotFound {
resp.Diagnostics.Append(diagutils.NewNotFoundError("issue alert"))
resp.State.RemoveResource(ctx)
return
- }
- if err != nil {
- resp.Diagnostics.Append(diagutils.NewClientError("read", err))
+ } else if httpResp.StatusCode() != http.StatusOK || httpResp.JSON200 == nil {
+ resp.Diagnostics.Append(diagutils.NewClientStatusError("read", httpResp.StatusCode(), httpResp.Body))
return
}
- if err := data.Fill(data.Organization.ValueString(), *action); err != nil {
- resp.Diagnostics.Append(diagutils.NewFillError(err))
+ resp.Diagnostics.Append(data.Fill(ctx, *httpResp.JSON200)...)
+ if resp.Diagnostics.HasError() {
return
}
@@ -255,55 +830,108 @@ func (r *IssueAlertResource) Read(ctx context.Context, req resource.ReadRequest,
}
func (r *IssueAlertResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
- var data IssueAlertResourceModel
+ var data IssueAlertModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
- params := &sentry.IssueAlert{
- Name: data.Name.ValueStringPointer(),
- ActionMatch: data.ActionMatch.ValueStringPointer(),
- FilterMatch: data.FilterMatch.ValueStringPointer(),
- Frequency: sentry.JsonNumber(json.Number(data.Frequency.String())),
+ body := apiclient.UpdateProjectRuleJSONRequestBody{
+ Name: data.Name.ValueString(),
+ ActionMatch: data.ActionMatch.ValueString(),
+ FilterMatch: data.FilterMatch.ValueString(),
+ Frequency: data.Frequency.ValueInt64(),
Owner: data.Owner.ValueStringPointer(),
Environment: data.Environment.ValueStringPointer(),
- Projects: []string{data.Project.String()},
+ Projects: []string{data.Project.ValueString()},
}
+
if !data.Conditions.IsNull() {
- resp.Diagnostics.Append(data.Conditions.Unmarshal(¶ms.Conditions)...)
+ resp.Diagnostics.Append(data.Conditions.Unmarshal(&body.Conditions)...)
+ } else if data.ConditionsV2 != nil {
+ body.Conditions = []apiclient.ProjectRuleCondition{}
+ for i, item := range *data.ConditionsV2 {
+ condition, diags := item.ToApi(ctx)
+ if diags.HasError() {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("conditions_v2").AtListIndex(i),
+ "Missing attribute configuration",
+ fmt.Sprintf("Failed to convert condition: %s", diags),
+ )
+ return
+ }
+ body.Conditions = append(body.Conditions, *condition)
+ }
+ } else {
+ body.Conditions = []apiclient.ProjectRuleCondition{}
}
+
if !data.Filters.IsNull() {
- resp.Diagnostics.Append(data.Filters.Unmarshal(¶ms.Filters)...)
+ resp.Diagnostics.Append(data.Filters.Unmarshal(&body.Filters)...)
+ } else if data.FiltersV2 != nil {
+ body.Filters = []apiclient.ProjectRuleFilter{}
+ for i, item := range *data.FiltersV2 {
+ filter, diags := item.ToApi(ctx)
+ if diags.HasError() {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("filters_v2").AtListIndex(i),
+ "Missing attribute configuration",
+ fmt.Sprintf("Failed to convert filter: %s", diags),
+ )
+ return
+ }
+ body.Filters = append(body.Filters, *filter)
+ }
+ } else {
+ body.Filters = []apiclient.ProjectRuleFilter{}
}
+
if !data.Actions.IsNull() {
- resp.Diagnostics.Append(data.Actions.Unmarshal(¶ms.Actions)...)
+ resp.Diagnostics.Append(data.Actions.Unmarshal(&body.Actions)...)
+ } else if data.ActionsV2 != nil {
+ body.Actions = []apiclient.ProjectRuleAction{}
+ for i, item := range *data.ActionsV2 {
+ action, diags := item.ToApi(ctx)
+ if diags.HasError() {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("actions_v2").AtListIndex(i),
+ "Missing attribute configuration",
+ fmt.Sprintf("Failed to convert action: %s", diags),
+ )
+ return
+ }
+ body.Actions = append(body.Actions, *action)
+ }
+ } else {
+ body.Actions = []apiclient.ProjectRuleAction{}
}
if resp.Diagnostics.HasError() {
return
}
- action, apiResp, err := r.client.IssueAlerts.Update(
+ httpResp, err := r.apiClient.UpdateProjectRuleWithResponse(
ctx,
data.Organization.ValueString(),
data.Project.ValueString(),
data.Id.ValueString(),
- params,
+ body,
)
- if apiResp.StatusCode == http.StatusNotFound {
+ if err != nil {
+ resp.Diagnostics.Append(diagutils.NewClientError("update", err))
+ return
+ } else if httpResp.StatusCode() == http.StatusNotFound {
resp.Diagnostics.Append(diagutils.NewNotFoundError("issue alert"))
resp.State.RemoveResource(ctx)
return
- }
- if err != nil {
- resp.Diagnostics.Append(diagutils.NewClientError("update", err))
+ } else if httpResp.StatusCode() != http.StatusOK || httpResp.JSON200 == nil {
+ resp.Diagnostics.Append(diagutils.NewClientStatusError("update", httpResp.StatusCode(), httpResp.Body))
return
}
- if err := data.Fill(data.Organization.ValueString(), *action); err != nil {
- resp.Diagnostics.Append(diagutils.NewFillError(err))
+ resp.Diagnostics.Append(data.Fill(ctx, *httpResp.JSON200)...)
+ if resp.Diagnostics.HasError() {
return
}
@@ -311,43 +939,32 @@ func (r *IssueAlertResource) Update(ctx context.Context, req resource.UpdateRequ
}
func (r *IssueAlertResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
- var data IssueAlertResourceModel
+ var data IssueAlertModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
- apiResp, err := r.client.IssueAlerts.Delete(
+ httpResp, err := r.apiClient.DeleteProjectRuleWithResponse(
ctx,
data.Organization.ValueString(),
data.Project.ValueString(),
data.Id.ValueString(),
)
- if apiResp.StatusCode == http.StatusNotFound {
- return
- }
if err != nil {
resp.Diagnostics.Append(diagutils.NewClientError("delete", err))
return
+ } else if httpResp.StatusCode() == http.StatusNotFound {
+ return
+ } else if httpResp.StatusCode() != http.StatusAccepted {
+ resp.Diagnostics.Append(diagutils.NewClientStatusError("delete", httpResp.StatusCode(), httpResp.Body))
+ return
}
}
func (r *IssueAlertResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
- organization, project, actionId, err := splitThreePartID(req.ID, "organization", "project-slug", "alert-id")
- if err != nil {
- resp.Diagnostics.Append(diagutils.NewFillError(err))
- return
- }
- resp.Diagnostics.Append(resp.State.SetAttribute(
- ctx, path.Root("organization"), organization,
- )...)
- resp.Diagnostics.Append(resp.State.SetAttribute(
- ctx, path.Root("project"), project,
- )...)
- resp.Diagnostics.Append(resp.State.SetAttribute(
- ctx, path.Root("id"), actionId,
- )...)
+ tfutils.ImportStateThreePartId(ctx, "organization", "project", req, resp)
}
func (r *IssueAlertResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader {
@@ -432,7 +1049,7 @@ func (r *IssueAlertResource) UpgradeState(ctx context.Context) map[int64]resourc
return
}
- upgradedStateData := IssueAlertResourceModel{
+ upgradedStateData := IssueAlertModel{
Id: types.StringValue(actionId),
Organization: types.StringValue(organization),
Project: types.StringValue(project),
diff --git a/internal/provider/resource_issue_alert_test.go b/internal/provider/resource_issue_alert_test.go
index 888bfdbc1..e2f276b51 100644
--- a/internal/provider/resource_issue_alert_test.go
+++ b/internal/provider/resource_issue_alert_test.go
@@ -4,19 +4,760 @@ import (
"context"
"errors"
"fmt"
+ "regexp"
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/knownvalue"
+ "github.com/hashicorp/terraform-plugin-testing/statecheck"
"github.com/hashicorp/terraform-plugin-testing/terraform"
+ "github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
"github.com/jianyuan/go-sentry/v2/sentry"
"github.com/jianyuan/terraform-provider-sentry/internal/acctest"
)
-func TestAccIssueAlertResource(t *testing.T) {
+func TestAccIssueAlertResource_validation(t *testing.T) {
+ team := acctest.RandomWithPrefix("tf-team")
+ project := acctest.RandomWithPrefix("tf-project")
+ alert := acctest.RandomWithPrefix("tf-issue-alert")
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.PreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccIssueAlertConfig(team, project, alert, ``),
+ ExpectError: acctest.ExpectLiteralError(`At least one of these attributes must be configured: [actions,actions_v2]`),
+ },
+ {
+ Config: testAccIssueAlertConfig(team, project, alert, `
+ actions = "[]"
+ `),
+ ExpectError: acctest.ExpectLiteralError(`You must add an action for this alert to fire`),
+ },
+ {
+ Config: testAccIssueAlertConfig(team, project, alert, `
+ actions_v2 = []
+ `),
+ ExpectError: acctest.ExpectLiteralError(`You must add an action for this alert to fire`),
+ },
+ {
+ Config: testAccIssueAlertConfig(team, project, alert, `
+ conditions = "[]"
+ conditions_v2 = []
+ `),
+ ExpectError: acctest.ExpectLiteralError(`Attribute "conditions" cannot be specified when "conditions_v2" is specified`),
+ },
+ {
+ Config: testAccIssueAlertConfig(team, project, alert, `
+ filters = "[]"
+ filters_v2 = []
+ `),
+ ExpectError: acctest.ExpectLiteralError(`Attribute "filters" cannot be specified when "filters_v2" is specified`),
+ },
+ {
+ Config: testAccIssueAlertConfig(team, project, alert, `
+ actions = "[]"
+ actions_v2 = []
+ `),
+ ExpectError: acctest.ExpectLiteralError(`Attribute "actions" cannot be specified when "actions_v2" is specified`),
+ },
+ {
+ Config: testAccIssueAlertConfig(team, project, alert, `
+ actions = "[]"
+ `),
+ ExpectError: acctest.ExpectLiteralError(`You must add an action for this alert to fire`),
+ },
+ {
+ Config: testAccIssueAlertConfig(team, project, alert, `
+ actions_v2 = []
+ `),
+ ExpectError: acctest.ExpectLiteralError(`You must add an action for this alert to fire`),
+ },
+ {
+ Config: testAccIssueAlertConfig(team, project, alert, `
+ actions_v2 = [{ }]
+ `),
+ ExpectError: acctest.ExpectLiteralError(
+ `Failed to convert action: [{Exactly one action must be set Exactly one action`,
+ `must be set}]`,
+ ),
+ },
+ {
+ Config: testAccIssueAlertConfig(team, project, alert, `
+ actions_v2 = [{ notify_event = { } }]
+
+ filters_v2 = [{ }]
+ `),
+ ExpectError: acctest.ExpectLiteralError(
+ `Failed to convert filter: [{Exactly one filter must be set Exactly one filter`,
+ `must be set}]`,
+ ),
+ },
+ {
+ Config: testAccIssueAlertConfig(team, project, alert, `
+ actions_v2 = [{ notify_event = { } }]
+
+ conditions_v2 = [{ }]
+ `),
+ ExpectError: acctest.ExpectLiteralError(
+ `Failed to convert condition: [{Exactly one condition must be set Exactly one`,
+ `condition must be set}]`,
+ ),
+ },
+ {
+ Config: testAccIssueAlertConfig(team, project, alert, `
+ actions_v2 = [{ notify_event = { } }]
+
+ conditions_v2 = [
+ { first_seen_event = {}, regression_event = {} },
+ ]
+ `),
+ ExpectError: acctest.ExpectLiteralError(
+ `Attribute "conditions_v2[0].first_seen_event" cannot be specified when`,
+ `"conditions_v2[0].regression_event" is specified`,
+ ),
+ },
+ },
+ })
+}
+
+func TestAccIssueAlertResource_basic(t *testing.T) {
+ rn := "sentry_issue_alert.test"
+ team := acctest.RandomWithPrefix("tf-team")
+ project := acctest.RandomWithPrefix("tf-project")
+ alert := acctest.RandomWithPrefix("tf-issue-alert")
+
+ checks := []statecheck.StateCheck{
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("id"), knownvalue.NotNull()),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("organization"), knownvalue.StringExact(acctest.TestOrganization)),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("project"), knownvalue.StringExact(project)),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("action_match"), knownvalue.StringExact("any")),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("filter_match"), knownvalue.StringExact("any")),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("frequency"), knownvalue.Int64Exact(30)),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("environment"), knownvalue.Null()),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("owner"), knownvalue.Null()),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("conditions"), knownvalue.Null()),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("filters"), knownvalue.Null()),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("actions"), knownvalue.Null()),
+ }
+
+ resource.Test(t, resource.TestCase{
+ // TODO: Precheck acctest.TestOpsgenieIntegrationKey
+ PreCheck: func() { acctest.PreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ CheckDestroy: testAccCheckIssueAlertDestroy,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccIssueAlertConfig(team, project, alert, `
+ conditions_v2 = [
+ { first_seen_event = {} },
+ { regression_event = {} },
+ { reappeared_event = {} },
+ { new_high_priority_issue = {} },
+ { existing_high_priority_issue = {} },
+ { event_frequency = { comparison_type = "count", value = 100, interval = "1h" } },
+ { event_frequency = { comparison_type = "percent", comparison_interval = "1w", value = 100, interval = "1h" } },
+ { event_unique_user_frequency = { comparison_type = "count", value = 100, interval = "1h" } },
+ { event_unique_user_frequency = { comparison_type = "percent", comparison_interval = "1w", value = 100, interval = "1h" } },
+ { event_frequency_percent = { comparison_type = "count", value = 100, interval = "1h" } },
+ { event_frequency_percent = { comparison_type = "percent", comparison_interval = "1w", value = 100, interval = "1h" } },
+ ]
+
+ filters_v2 = [
+ { age_comparison = { comparison_type = "older", value = 10, time = "minute" } },
+ { issue_occurrences = { value = 10 } },
+ { assigned_to = { target_type = "Unassigned" } },
+ { assigned_to = { target_type = "Team", target_identifier = sentry_team.test.internal_id } },
+ { latest_adopted_release = { oldest_or_newest = "oldest", older_or_newer = "older", environment = "test" } },
+ { latest_release = {} },
+ { issue_category = { value = "Error" } },
+ { event_attribute = { attribute = "message", match = "CONTAINS", value = "test" } },
+ { event_attribute = { attribute = "message", match = "IS_SET" } },
+ { tagged_event = { key = "key", match = "CONTAINS", value = "value" } },
+ { tagged_event = { key = "key", match = "NOT_SET" } },
+ { level = { match = "EQUAL", level = "error" } },
+ ]
+
+ actions_v2 = [
+ { notify_email = { target_type = "IssueOwners", fallthrough_type = "ActiveMembers" } },
+ { notify_email = { target_type = "Team", target_identifier = sentry_team.test.internal_id } },
+ { notify_event = { } },
+ {
+ notify_event_service = {
+ service = "terraform-provider-sentry-ea4fdd"
+ }
+ },
+ {
+ notify_event_sentry_app = {
+ sentry_app_installation_uuid = "d384d654-0e4c-447d-999c-a298fad579a7"
+
+ settings = {
+ teamId = "5538c20b-37cf-4efd-b0aa-83c7f2e691f8"
+ assigneeId = "b7afdd84-58b9-48ab-a682-9bb121d9dfbd"
+ labelId = "9f918fa3-9641-4522-950e-84dfb5c21099"
+ projectId = ""
+ stateId = "23e412bc-5abc-4812-916c-f91b4e21a060"
+ priority = "0"
+ }
+ }
+ },
+ {
+ opsgenie_notify_team = {
+ account = sentry_integration_opsgenie.opsgenie.integration_id
+ team = sentry_integration_opsgenie.opsgenie.id
+ priority = "P1"
+ }
+ },
+ {
+ pagerduty_notify_service = {
+ account = sentry_integration_pagerduty.pagerduty.integration_id
+ service = sentry_integration_pagerduty.pagerduty.id
+ severity = "default"
+ }
+ },
+ {
+ slack_notify_service = {
+ workspace = data.sentry_organization_integration.slack.id
+ channel = "#general"
+ notes = "Please for triage information"
+ }
+ },
+ {
+ slack_notify_service = {
+ workspace = data.sentry_organization_integration.slack.id
+ channel = "#general"
+ tags = ["environment", "level"]
+ notes = "Please for triage information"
+ }
+ },
+ {
+ discord_notify_service = {
+ server = data.sentry_organization_integration.discord.id
+ channel_id = "714123428994482189"
+ }
+ },
+ {
+ discord_notify_service = {
+ server = data.sentry_organization_integration.discord.id
+ channel_id = "714123428994482189"
+ tags = ["environment", "level"]
+ }
+ },
+ {
+ github_create_ticket = {
+ integration = data.sentry_organization_integration.github.id
+ repo = "terraform-provider-sentry"
+ assignee = "jianyuan"
+ labels = ["bug", "enhancement"]
+ }
+ },
+ {
+ azure_devops_create_ticket = {
+ integration = data.sentry_organization_integration.vsts.id
+ project = "123"
+ work_item_type = "Microsoft.VSTS.WorkItemTypes.Task"
+ }
+ }
+ ]
+ `) + fmt.Sprintf(`
+ # Opsgenie
+ data "sentry_organization_integration" "opsgenie" {
+ organization = sentry_project.test.organization
+ provider_key = "opsgenie"
+ name = "terraform-provider-sentry"
+ }
+
+ resource "sentry_integration_opsgenie" "opsgenie" {
+ organization = data.sentry_organization_integration.opsgenie.organization
+ integration_id = data.sentry_organization_integration.opsgenie.id
+ team = "issue-alert-team"
+ integration_key = "%[1]s"
+ }
+
+ # PagerDuty
+ data "sentry_organization_integration" "pagerduty" {
+ organization = sentry_project.test.organization
+ provider_key = "pagerduty"
+ name = "terraform-provider-sentry"
+ }
+
+ resource "sentry_integration_pagerduty" "pagerduty" {
+ organization = data.sentry_organization_integration.pagerduty.organization
+ integration_id = data.sentry_organization_integration.pagerduty.id
+ service = "issue-alert-service"
+ integration_key = "issue-alert-integration-key"
+ }
+
+ # Slack
+ data "sentry_organization_integration" "slack" {
+ organization = sentry_project.test.organization
+ provider_key = "slack"
+ name = "A2 Marketing" # TODO: Use a real integration name
+ }
+
+ # Discord
+ data "sentry_organization_integration" "discord" {
+ organization = sentry_project.test.organization
+ provider_key = "discord"
+ name = "jy's server"
+ }
+
+ # GitHub
+ data "sentry_organization_integration" "github" {
+ organization = sentry_project.test.organization
+ provider_key = "github"
+ name = "jianyuan"
+ }
+
+ # Azure DevOps
+ data "sentry_organization_integration" "vsts" {
+ organization = sentry_project.test.organization
+ provider_key = "vsts"
+ name = "jianyuanlee"
+ }
+ `, acctest.TestOpsgenieIntegrationKey),
+ ConfigStateChecks: append(
+ checks,
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("name"), knownvalue.StringExact(alert)),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("conditions_v2"), knownvalue.ListExact([]knownvalue.Check{
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "first_seen_event": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "regression_event": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "reappeared_event": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "new_high_priority_issue": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "existing_high_priority_issue": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "event_frequency": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "comparison_type": knownvalue.StringExact("count"),
+ "comparison_interval": knownvalue.Null(),
+ "value": knownvalue.Int64Exact(100),
+ "interval": knownvalue.StringExact("1h"),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "event_frequency": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "comparison_type": knownvalue.StringExact("percent"),
+ "comparison_interval": knownvalue.StringExact("1w"),
+ "value": knownvalue.Int64Exact(100),
+ "interval": knownvalue.StringExact("1h"),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "event_unique_user_frequency": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "comparison_type": knownvalue.StringExact("count"),
+ "comparison_interval": knownvalue.Null(),
+ "value": knownvalue.Int64Exact(100),
+ "interval": knownvalue.StringExact("1h"),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "event_unique_user_frequency": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "comparison_type": knownvalue.StringExact("percent"),
+ "comparison_interval": knownvalue.StringExact("1w"),
+ "value": knownvalue.Int64Exact(100),
+ "interval": knownvalue.StringExact("1h"),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "event_frequency_percent": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "comparison_type": knownvalue.StringExact("count"),
+ "comparison_interval": knownvalue.Null(),
+ "value": knownvalue.Float64Exact(100),
+ "interval": knownvalue.StringExact("1h"),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "event_frequency_percent": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "comparison_type": knownvalue.StringExact("percent"),
+ "comparison_interval": knownvalue.StringExact("1w"),
+ "value": knownvalue.Float64Exact(100),
+ "interval": knownvalue.StringExact("1h"),
+ }),
+ }),
+ })),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("filters_v2"), knownvalue.ListExact([]knownvalue.Check{
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "age_comparison": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "comparison_type": knownvalue.StringExact("older"),
+ "value": knownvalue.Int64Exact(10),
+ "time": knownvalue.StringExact("minute"),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "issue_occurrences": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "value": knownvalue.Int64Exact(10),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "assigned_to": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "target_type": knownvalue.StringExact("Unassigned"),
+ "target_identifier": knownvalue.Null(),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "assigned_to": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "target_type": knownvalue.StringExact("Team"),
+ "target_identifier": knownvalue.StringRegexp(regexp.MustCompile(`^\d+$`)),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "latest_adopted_release": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "oldest_or_newest": knownvalue.StringExact("oldest"),
+ "older_or_newer": knownvalue.StringExact("older"),
+ "environment": knownvalue.StringExact("test"),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "latest_release": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "issue_category": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "value": knownvalue.StringExact("Error"),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "event_attribute": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "attribute": knownvalue.StringExact("message"),
+ "match": knownvalue.StringExact("CONTAINS"),
+ "value": knownvalue.StringExact("test"),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "event_attribute": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "attribute": knownvalue.StringExact("message"),
+ "match": knownvalue.StringExact("IS_SET"),
+ "value": knownvalue.Null(),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "tagged_event": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "key": knownvalue.StringExact("key"),
+ "match": knownvalue.StringExact("CONTAINS"),
+ "value": knownvalue.StringExact("value"),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "tagged_event": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "key": knownvalue.StringExact("key"),
+ "match": knownvalue.StringExact("NOT_SET"),
+ "value": knownvalue.Null(),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "level": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "match": knownvalue.StringExact("EQUAL"),
+ "level": knownvalue.StringExact("error"),
+ }),
+ }),
+ })),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("actions_v2"), knownvalue.ListExact([]knownvalue.Check{
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "notify_email": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "target_type": knownvalue.StringExact("IssueOwners"),
+ "target_identifier": knownvalue.Null(),
+ "fallthrough_type": knownvalue.StringExact("ActiveMembers"),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "notify_email": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "target_type": knownvalue.StringExact("Team"),
+ "target_identifier": knownvalue.NotNull(),
+ "fallthrough_type": knownvalue.Null(),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "notify_event": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "notify_event_service": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "service": knownvalue.StringExact("terraform-provider-sentry-ea4fdd"),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "notify_event_sentry_app": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "sentry_app_installation_uuid": knownvalue.StringExact("d384d654-0e4c-447d-999c-a298fad579a7"),
+ "settings": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "teamId": knownvalue.StringExact("5538c20b-37cf-4efd-b0aa-83c7f2e691f8"),
+ "assigneeId": knownvalue.StringExact("b7afdd84-58b9-48ab-a682-9bb121d9dfbd"),
+ "labelId": knownvalue.StringExact("9f918fa3-9641-4522-950e-84dfb5c21099"),
+ "projectId": knownvalue.StringExact(""),
+ "stateId": knownvalue.StringExact("23e412bc-5abc-4812-916c-f91b4e21a060"),
+ "priority": knownvalue.StringExact("0"),
+ }),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "opsgenie_notify_team": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "account": knownvalue.NotNull(),
+ "team": knownvalue.NotNull(),
+ "priority": knownvalue.StringExact("P1"),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "pagerduty_notify_service": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "account": knownvalue.NotNull(),
+ "service": knownvalue.NotNull(),
+ "severity": knownvalue.StringExact("default"),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "slack_notify_service": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "workspace": knownvalue.NotNull(),
+ "channel": knownvalue.StringExact("#general"),
+ "channel_id": knownvalue.NotNull(),
+ "tags": knownvalue.Null(),
+ "notes": knownvalue.StringExact("Please for triage information"),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "slack_notify_service": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "workspace": knownvalue.NotNull(),
+ "channel": knownvalue.StringExact("#general"),
+ "channel_id": knownvalue.NotNull(),
+ "tags": knownvalue.SetExact([]knownvalue.Check{
+ knownvalue.StringExact("environment"),
+ knownvalue.StringExact("level"),
+ }),
+ "notes": knownvalue.StringExact("Please for triage information"),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "discord_notify_service": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "server": knownvalue.NotNull(),
+ "channel_id": knownvalue.NotNull(),
+ "tags": knownvalue.Null(),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "discord_notify_service": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "server": knownvalue.NotNull(),
+ "channel_id": knownvalue.NotNull(),
+ "tags": knownvalue.SetExact([]knownvalue.Check{
+ knownvalue.StringExact("environment"),
+ knownvalue.StringExact("level"),
+ }),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "github_create_ticket": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "integration": knownvalue.NotNull(),
+ "repo": knownvalue.StringExact("terraform-provider-sentry"),
+ "assignee": knownvalue.StringExact("jianyuan"),
+ "labels": knownvalue.SetExact([]knownvalue.Check{
+ knownvalue.StringExact("bug"),
+ knownvalue.StringExact("enhancement"),
+ }),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "azure_devops_create_ticket": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "integration": knownvalue.NotNull(),
+ "project": knownvalue.StringExact("123"),
+ "work_item_type": knownvalue.StringExact("Microsoft.VSTS.WorkItemTypes.Task"),
+ }),
+ }),
+ })),
+ ),
+ },
+ {
+ Config: testAccIssueAlertConfig(team, project, alert+"-updated", `
+ conditions_v2 = [
+ { reappeared_event = {} },
+ { new_high_priority_issue = {} },
+ { existing_high_priority_issue = {} },
+ ]
+ filters_v2 = []
+ actions_v2 = [
+ { notify_email = { target_type = "IssueOwners", fallthrough_type = "NoOne" } },
+ ]
+ `),
+ ConfigStateChecks: append(
+ checks,
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("name"), knownvalue.StringExact(alert+"-updated")),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("conditions_v2"), knownvalue.ListExact([]knownvalue.Check{
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "reappeared_event": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "new_high_priority_issue": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ }),
+ }),
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "existing_high_priority_issue": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ }),
+ }),
+ })),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("filters_v2"), knownvalue.ListExact([]knownvalue.Check{})),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("actions_v2"), knownvalue.ListExact([]knownvalue.Check{
+ knownvalue.ObjectPartial(map[string]knownvalue.Check{
+ "notify_email": knownvalue.ObjectExact(map[string]knownvalue.Check{
+ "name": knownvalue.NotNull(),
+ "target_type": knownvalue.StringExact("IssueOwners"),
+ "target_identifier": knownvalue.Null(),
+ "fallthrough_type": knownvalue.StringExact("NoOne"),
+ }),
+ }),
+ })),
+ ),
+ },
+ {
+ ResourceName: rn,
+ ImportState: true,
+ ImportStateIdFunc: acctest.ThreePartImportStateIdFunc(rn, "organization", "project"),
+ },
+ },
+ })
+}
+
+func TestAccIssueAlertResource_emptyArray(t *testing.T) {
+ rn := "sentry_issue_alert.test"
+ team := acctest.RandomWithPrefix("tf-team")
+ project := acctest.RandomWithPrefix("tf-project")
+ alert := acctest.RandomWithPrefix("tf-issue-alert")
+ var alertId string
+
+ check := func(alert string) resource.TestCheckFunc {
+ return resource.ComposeTestCheckFunc(
+ testAccCheckIssueAlertExists(rn, &alertId),
+ resource.TestCheckResourceAttrWith(rn, "id", func(value string) error {
+ if alertId != value {
+ return fmt.Errorf("expected %s, got %s", alertId, value)
+ }
+ return nil
+ }),
+ )
+ }
+
+ checks := []statecheck.StateCheck{
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("id"), knownvalue.NotNull()),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("organization"), knownvalue.StringExact(acctest.TestOrganization)),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("project"), knownvalue.StringExact(project)),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("action_match"), knownvalue.StringExact("any")),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("filter_match"), knownvalue.StringExact("any")),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("frequency"), knownvalue.Int64Exact(30)),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("environment"), knownvalue.Null()),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("owner"), knownvalue.Null()),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("conditions"), knownvalue.Null()),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("filters"), knownvalue.Null()),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("actions"), knownvalue.NotNull()),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("conditions_v2"), knownvalue.ListSizeExact(0)),
+ statecheck.ExpectKnownValue(rn, tfjsonpath.New("filters_v2"), knownvalue.Null()),
+ }
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.PreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ CheckDestroy: testAccCheckIssueAlertDestroy,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccIssueAlertConfig(team, project, alert, `
+ conditions_v2 = []
+
+ actions = < 0 {
+ parsedItems = append(parsedItems, parsedItem)
+ }
+ }
+
+ return strings.Join(parsedItems, ","), diags
+}
+
+func (v StringSet) ValueStringPointer(ctx context.Context) (*string, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ if v.IsNull() || v.IsUnknown() || len(v.Elements()) == 0 {
+ return nil, diags
+ }
+
+ var items []types.String
+ diags.Append(v.ElementsAs(ctx, &items, false)...)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ var parsedItems []string
+ for _, item := range items {
+ if item.IsNull() || item.IsUnknown() {
+ continue
+ }
+
+ parsedItem := strings.TrimSpace(item.ValueString())
+ if len(parsedItem) > 0 {
+ parsedItems = append(parsedItems, parsedItem)
+ }
+ }
+
+ return ptr.Ptr(strings.Join(parsedItems, ",")), diags
+}
+
+func StringSetNull() StringSet {
+ return StringSet{SetValue: basetypes.NewSetNull(types.StringType)}
+}
+
+func StringSetUnknown() StringSet {
+ return StringSet{SetValue: basetypes.NewSetUnknown(types.StringType)}
+}
+
+func StringSetPointerValue(value *string) (StringSet, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ if value == nil || strings.TrimSpace(*value) == "" {
+ return StringSetNull(), diags
+ }
+
+ items := strings.Split(*value, ",")
+ elements := sliceutils.Map(func(item string) attr.Value {
+ return types.StringValue(strings.TrimSpace(item))
+ }, items)
+
+ setValue, d := types.SetValue(types.StringType, elements)
+ diags.Append(d...)
+
+ return StringSet{SetValue: setValue}, diags
+}
diff --git a/internal/tfutils/enum_string_attribute.go b/internal/tfutils/enum_string_attribute.go
new file mode 100644
index 000000000..2903c3175
--- /dev/null
+++ b/internal/tfutils/enum_string_attribute.go
@@ -0,0 +1,29 @@
+package tfutils
+
+import (
+ "strings"
+
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/jianyuan/go-utils/sliceutils"
+)
+
+func WithEnumStringAttribute(base schema.StringAttribute, choices []string) schema.StringAttribute {
+ // Add a markdown description that lists the valid values
+ if base.MarkdownDescription != "" {
+ base.MarkdownDescription += " "
+ }
+ validValues := sliceutils.Map(func(v string) string {
+ return "`" + v + "`"
+ }, choices)
+ if len(validValues) > 1 {
+ base.MarkdownDescription += "Valid values are: " + strings.Join(validValues[:len(validValues)-1], ", ") + ", and " + validValues[len(validValues)-1] + "."
+ } else {
+ base.MarkdownDescription += "Valid values are: " + validValues[0] + "."
+ }
+
+ // Add a validator that checks the value is one of the valid values
+ base.Validators = append(base.Validators, stringvalidator.OneOf(choices...))
+
+ return base
+}
diff --git a/internal/tfutils/merge_diagnostics.go b/internal/tfutils/merge_diagnostics.go
new file mode 100644
index 000000000..b14289363
--- /dev/null
+++ b/internal/tfutils/merge_diagnostics.go
@@ -0,0 +1,12 @@
+package tfutils
+
+import "github.com/hashicorp/terraform-plugin-framework/diag"
+
+// MergeDiagnostics is a utility function that merges the given diagnostics into the
+// provided diagnostics and returns the original value.
+func MergeDiagnostics[T any](v T, diagsOut diag.Diagnostics) func(diags *diag.Diagnostics) T {
+ return func(diags *diag.Diagnostics) T {
+ diags.Append(diagsOut...)
+ return v
+ }
+}
diff --git a/internal/tfutils/mutex.go b/internal/tfutils/mutex.go
new file mode 100644
index 000000000..0942a9fe8
--- /dev/null
+++ b/internal/tfutils/mutex.go
@@ -0,0 +1,35 @@
+package tfutils
+
+import (
+ "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+)
+
+func WithMutuallyExclusiveValidator(attributes map[string]schema.SingleNestedAttribute) map[string]schema.Attribute {
+ var names []string
+ for name := range attributes {
+ names = append(names, name)
+ }
+
+ conditionFor := func(name string) []validator.Object {
+ var paths []path.Expression
+
+ for _, thisName := range names {
+ if thisName != name {
+ paths = append(paths, path.MatchRelative().AtParent().AtName(thisName))
+ }
+ }
+
+ return []validator.Object{objectvalidator.ConflictsWith(paths...)}
+ }
+
+ result := make(map[string]schema.Attribute, len(attributes))
+ for name, attribute := range attributes {
+ attribute.Validators = append(attribute.Validators, conditionFor(name)...)
+ result[name] = attribute
+ }
+
+ return result
+}