Add read-only data sources for all three Terraform resources

Mechanical, low-risk follow-up -- no new architectural question, no new
external service, no new write path. Each of sentry_dashboard,
sentry_alert_rule, and sentry_notification_target gets a matching data
source: a single Required id attribute in, every other attribute
Computed out, backed by the exact same getDashboard/getRule/
getNotificationTarget client methods and dashboardModelFromAPI/
alertRuleModelFromAPI/notificationTargetModelFromAPI conversion
functions the resources already use and already have tests for -- these
data sources add no new client code at all, just a thin
datasource.DataSource wrapper reusing what Create/Read/Update/Delete
already exercise.

sentry_notification_target's data source carries the same secret
caveat its resource does (Sensitive, but alerting's GET /targets/{id}
returns it unredacted, so it's a real plaintext value in Terraform
state) -- named again here rather than assumed obvious from the
resource's own docs.

Verified: provider_test.go's new schema-validation tests confirm each
data source's id is Required and everything else Computed, no
Terraform binary needed. Three new real acceptance tests
(TestAccDashboardDataSource_basic and its two siblings) each create a
resource then look it up via the matching data source, using
resource.TestCheckResourceAttrPair to prove the data source's Read
actually agrees with what the resource wrote -- not just that both
compile. Skip-gated by TF_ACC same as the existing six acceptance
tests, and needs the same live api/alerting services this environment
has no Docker access to bring up, so not run here -- same disclosed gap
as everything else Docker-gated in this repo.
This commit is contained in:
2026-08-15 10:28:08 -07:00
parent 30ae84cd04
commit eb38611aa8
13 changed files with 556 additions and 30 deletions
+7 -6
View File
@@ -23,12 +23,13 @@ described there without flagging it to me first.
as of this note -- three resources (`sentry_dashboard`, full CRUD; as of this note -- three resources (`sentry_dashboard`, full CRUD;
`sentry_alert_rule` and `sentry_notification_target`, both create/ `sentry_alert_rule` and `sentry_notification_target`, both create/
destroy only -- `alerting` has no `PUT /rules/{id}` or destroy only -- `alerting` has no `PUT /rules/{id}` or
`PUT /targets/{id}` to update against), built on HashiCorp's `PUT /targets/{id}` to update against), each paired with a read-only
`terraform-plugin-framework`, reusing the exact same REST contracts data source, built on HashiCorp's `terraform-plugin-framework`,
`sentryctl dashboards apply`/web's dashboard export and reusing the exact same REST contracts `sentryctl dashboards apply`/
`sentryctl alerts apply` already use. Tenant/RBAC resources are real, web's dashboard export and `sentryctl alerts apply` already use.
disclosed future work -- see `/terraform/README.md` for the full Tenant/RBAC resources are real, disclosed future work -- see
accounting of what is and isn't built, and the same `/terraform/README.md` for the full accounting of what is and isn't
built, and the same
"written but not run against a live stack" verification caveat as "written but not run against a live stack" verification caveat as
everything else Docker-gated in this repo. everything else Docker-gated in this repo.
+53 -23
View File
@@ -5,7 +5,8 @@ names this a first-class deliverable alongside `sentryctl`
("CLI and Terraform provider are first-class, not afterthoughts"), but ("CLI and Terraform provider are first-class, not afterthoughts"), but
no phase before this one had actually built any of it. Three resources no phase before this one had actually built any of it. Three resources
so far (`sentry_dashboard`, `sentry_alert_rule`, so far (`sentry_dashboard`, `sentry_alert_rule`,
`sentry_notification_target`), not a finished provider, built on `sentry_notification_target`), each paired with a read-only data source
of the same name, not a finished provider, built on
[HashiCorp's `terraform-plugin-framework`][framework] (the [HashiCorp's `terraform-plugin-framework`][framework] (the
actively-developed library, not the legacy SDKv2 -- there's no existing actively-developed library, not the legacy SDKv2 -- there's no existing
provider code here to migrate, so there's no reason to start on the provider code here to migrate, so there's no reason to start on the
@@ -130,6 +131,35 @@ the standard, well-known Terraform caveat for any sensitive attribute
(encrypt the state backend, restrict who can read it), named here rather (encrypt the state backend, restrict who can read it), named here rather
than left implicit. than left implicit.
## Data sources
Each resource above has a matching read-only data source (`data
"sentry_dashboard"`, `data "sentry_alert_rule"`, `data
"sentry_notification_target"`), a single `id` attribute in, every other
attribute out -- a lookup against the same `GET`/`{id}` endpoint the
matching resource's own `Read` already uses, nothing new added to
`client.go` beyond that. Mechanical and low-risk by design: no new
architectural question, no new external service, no new write path --
just reusing the resource's own model/conversion functions
(`dashboardModelFromAPI` etc.) against a `Required` `id` input instead
of a full config.
```hcl
data "sentry_notification_target" "ops" {
id = "target-abc123"
}
resource "sentry_alert_rule" "checkout_5xx" {
# ...
notification_target_id = data.sentry_notification_target.ops.id
}
```
`sentry_notification_target`'s data source has the same `secret`
caveat its resource does -- `Sensitive`, but a real value visible in
Terraform state; see "`sentry_notification_target`'s `secret`
attribute" above.
**Also not built, all real and disclosed, not attempted here:** **Also not built, all real and disclosed, not attempted here:**
- Tenant/RBAC resources (`enterprise-auth`'s tenant/membership/grant - Tenant/RBAC resources (`enterprise-auth`'s tenant/membership/grant
surface) -- meaningfully different auth model (offline operator flags surface) -- meaningfully different auth model (offline operator flags
@@ -137,9 +167,6 @@ than left implicit.
idempotently -- see `/enterprise/README.md`'s "Bootstrapping a tenant" idempotently -- see `/enterprise/README.md`'s "Bootstrapping a tenant"
section) and Phase 4 commercial licensing, so this would need its own section) and Phase 4 commercial licensing, so this would need its own
design pass, not just "add another resource file." design pass, not just "add another resource file."
- Data sources (read-only lookup by ID/name) for any of the three
resources -- straightforward given the resources already exist, just
not built yet.
- Dashboard panels (see "Panels are not managed by this resource" above). - Dashboard panels (see "Panels are not managed by this resource" above).
- Publishing to the real Terraform Registry -- `main.go`'s `Address` - Publishing to the real Terraform Registry -- `main.go`'s `Address`
(`registry.terraform.io/sentry/sentry`) is the address a real (`registry.terraform.io/sentry/sentry`) is the address a real
@@ -167,26 +194,29 @@ extra `"state"` key this client deliberately has no field for still
parse cleanly, and (for targets) proving `secret` really does come back parse cleanly, and (for targets) proving `secret` really does come back
unredacted -- documenting real `alerting` behavior with a test, not just unredacted -- documenting real `alerting` behavior with a test, not just
a comment, so a future change to that behavior would be caught here too. a comment, so a future change to that behavior would be caught here too.
`internal/provider/provider_test.go` validates the provider and all `internal/provider/provider_test.go` validates the provider's, all
three resource schemas are internally well-formed (attribute names, the three resources', and all three data sources' schemas are internally
`Required`/`Computed`/`Sensitive` split) without needing a Terraform well-formed (attribute names, the `Required`/`Computed`/`Sensitive`
binary or a live `api`/`alerting` service at all. split -- for data sources, specifically that `id` is `Required` and
everything else is `Computed`) without needing a Terraform binary or a
live `api`/`alerting` service at all.
`internal/provider/dashboard_resource_test.go`'s Each resource and data source pair has a matching `TestAcc*_basic` in
`TestAccDashboardResource_basic`, its own `_test.go` file (`dashboard_resource_test.go`/
`internal/provider/alert_rule_resource_test.go`'s `dashboard_data_source_test.go`, and likewise for the other two) -- six
`TestAccAlertRuleResource_basic`, and real acceptance tests total, using [`terraform-plugin-testing`][testing]
`internal/provider/notification_target_resource_test.go`'s -- skipped unless `TF_ACC=1` is set, that framework's own standard
`TestAccNotificationTargetResource_basic` are real acceptance tests convention, the same shape every other live-infrastructure test in this
using [`terraform-plugin-testing`][testing] -- skipped unless `TF_ACC=1` repo uses (`docker`-gated env vars for Postgres/ClickHouse tests
is set, that framework's own standard convention, the same shape every elsewhere). The rule and target *resource* tests both use a
other live-infrastructure test in this repo uses (`docker`-gated env `plancheck.ExpectResourceAction` assertion proving a config change
vars for Postgres/ClickHouse tests elsewhere). The rule and target tests actually plans a destroy-then-create, not an in-place update -- the
both use a `plancheck.ExpectResourceAction` assertion proving a config concrete, checked version of the "create/destroy only" design decision
change actually plans a destroy-then-create, not an in-place update -- documented above, not just a claim in a comment; the *data source*
the concrete, checked version of the "create/destroy only" design tests each create a resource then look it up via `resource.
decision documented above, not just a claim in a comment. Even with TestCheckResourceAttrPair`, proving the data source's `Read` actually
`TF_ACC=1` all three tests also need real running `api`/`alerting` agrees with what the resource wrote, not just that both compile. Even
with `TF_ACC=1` all six tests also need real running `api`/`alerting`
services (Postgres + ClickHouse) to apply against, which this services (Postgres + ClickHouse) to apply against, which this
environment has no Docker access to bring up -- **not run here**, same environment has no Docker access to bring up -- **not run here**, same
disclosed gap as every other live-infra test across this repo (see disclosed gap as every other live-infra test across this repo (see
@@ -0,0 +1,7 @@
data "sentry_alert_rule" "checkout_5xx" {
id = "an-existing-rule-id"
}
output "checkout_5xx_notification_target" {
value = data.sentry_alert_rule.checkout_5xx.notification_target_id
}
@@ -0,0 +1,7 @@
data "sentry_dashboard" "checkout_errors" {
id = "an-existing-dashboard-id"
}
output "checkout_errors_default_earliest" {
value = data.sentry_dashboard.checkout_errors.default_earliest
}
@@ -0,0 +1,16 @@
# secret comes back unredacted (see the sentry_notification_target
# resource's schema doc comment) -- this data source's "secret"
# attribute is Sensitive for the same reason.
data "sentry_notification_target" "ops_webhook" {
id = "an-existing-target-id"
}
resource "sentry_alert_rule" "checkout_5xx" {
name = "Checkout 5xx spike"
query = "service=checkout status>=500 | stats count"
condition_type = "threshold"
comparator = "gt"
threshold_value = 50
eval_interval_seconds = 60
notification_target_id = data.sentry_notification_target.ops_webhook.id
}
@@ -0,0 +1,87 @@
package provider
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
)
var (
_ datasource.DataSource = &alertRuleDataSource{}
_ datasource.DataSourceWithConfigure = &alertRuleDataSource{}
)
func newAlertRuleDataSource() datasource.DataSource {
return &alertRuleDataSource{}
}
// alertRuleDataSource looks up an existing alert rule by ID -- see
// dashboardDataSource's doc comment for why this reuses
// alertRuleResourceModel/alertRuleModelFromAPI rather than a parallel
// type.
type alertRuleDataSource struct {
client *client
}
func (d *alertRuleDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_alert_rule"
}
func (d *alertRuleDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Looks up an existing Sentry alert rule by ID. See the sentry_alert_rule resource for how one is created/managed.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Required: true,
Description: "Rule ID to look up.",
},
"tenant_id": schema.StringAttribute{Computed: true},
"name": schema.StringAttribute{Computed: true},
"description": schema.StringAttribute{Computed: true},
"query": schema.StringAttribute{Computed: true},
"query_language": schema.StringAttribute{Computed: true},
"condition_type": schema.StringAttribute{Computed: true},
"comparator": schema.StringAttribute{Computed: true},
"threshold_value": schema.Float64Attribute{Computed: true},
"eval_interval_seconds": schema.Int64Attribute{Computed: true},
"for_minutes": schema.Int64Attribute{Computed: true},
"renotify_interval_minutes": schema.Int64Attribute{Computed: true},
"notification_target_id": schema.StringAttribute{Computed: true},
"enabled": schema.BoolAttribute{Computed: true},
"created_by": schema.StringAttribute{Computed: true},
},
}
}
func (d *alertRuleDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
data, ok := req.ProviderData.(*providerData)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *provider.providerData, got: %T. This is a provider bug -- please report it.", req.ProviderData),
)
return
}
d.client = data.alerting
}
func (d *alertRuleDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config alertRuleResourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
out, err := d.client.getRule(ctx, config.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Reading Alert Rule", err.Error())
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, alertRuleModelFromAPI(out))...)
}
@@ -0,0 +1,45 @@
package provider
import (
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)
// Same skip-gated-not-faked posture as TestAccAlertRuleResource_basic --
// see that test's doc comment. notification_target_id is a placeholder,
// same caveat as the resource test.
func TestAccAlertRuleDataSource_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: `
provider "sentry" {
endpoint = "http://localhost:8080"
alerting_endpoint = "http://localhost:8081"
}
resource "sentry_alert_rule" "test" {
name = "Data Source Test Rule"
query = "status>=500 | stats count"
condition_type = "threshold"
comparator = "gt"
threshold_value = 5
eval_interval_seconds = 60
notification_target_id = "placeholder-target-id"
}
data "sentry_alert_rule" "test" {
id = sentry_alert_rule.test.id
}
`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrPair("data.sentry_alert_rule.test", "name", "sentry_alert_rule.test", "name"),
resource.TestCheckResourceAttrPair("data.sentry_alert_rule.test", "query", "sentry_alert_rule.test", "query"),
resource.TestCheckResourceAttrPair("data.sentry_alert_rule.test", "threshold_value", "sentry_alert_rule.test", "threshold_value"),
),
},
},
})
}
@@ -0,0 +1,84 @@
package provider
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
)
var (
_ datasource.DataSource = &dashboardDataSource{}
_ datasource.DataSourceWithConfigure = &dashboardDataSource{}
)
func newDashboardDataSource() datasource.DataSource {
return &dashboardDataSource{}
}
// dashboardDataSource looks up an existing dashboard by ID -- read-only,
// GET /dashboards/{id} only, no lifecycle of its own. Reuses
// dashboardResourceModel/dashboardModelFromAPI from
// dashboard_resource.go directly rather than defining a parallel type:
// a data source's attribute set here is exactly the resource's (every
// field Computed except id, which the caller supplies), so there's
// nothing a second struct would express that the first doesn't already.
type dashboardDataSource struct {
client *client
}
func (d *dashboardDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_dashboard"
}
func (d *dashboardDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Looks up an existing Sentry dashboard by ID. See the sentry_dashboard resource for how one is created/managed.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Required: true,
Description: "Dashboard ID to look up.",
},
"tenant_id": schema.StringAttribute{Computed: true},
"name": schema.StringAttribute{Computed: true},
"description": schema.StringAttribute{Computed: true},
"default_earliest": schema.StringAttribute{Computed: true},
"default_latest": schema.StringAttribute{Computed: true},
"created_by": schema.StringAttribute{Computed: true},
"created_at": schema.StringAttribute{Computed: true},
"updated_at": schema.StringAttribute{Computed: true},
},
}
}
func (d *dashboardDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
data, ok := req.ProviderData.(*providerData)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *provider.providerData, got: %T. This is a provider bug -- please report it.", req.ProviderData),
)
return
}
d.client = data.api
}
func (d *dashboardDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config dashboardResourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
out, err := d.client.getDashboard(ctx, config.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Reading Dashboard", err.Error())
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, dashboardModelFromAPI(out))...)
}
@@ -0,0 +1,38 @@
package provider
import (
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)
// Same skip-gated-not-faked posture as TestAccDashboardResource_basic --
// see that test's doc comment.
func TestAccDashboardDataSource_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: `
provider "sentry" {
endpoint = "http://localhost:8080"
}
resource "sentry_dashboard" "test" {
name = "Data Source Test Dashboard"
description = "created by TestAccDashboardDataSource_basic"
}
data "sentry_dashboard" "test" {
id = sentry_dashboard.test.id
}
`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrPair("data.sentry_dashboard.test", "name", "sentry_dashboard.test", "name"),
resource.TestCheckResourceAttrPair("data.sentry_dashboard.test", "tenant_id", "sentry_dashboard.test", "tenant_id"),
resource.TestCheckResourceAttrPair("data.sentry_dashboard.test", "default_earliest", "sentry_dashboard.test", "default_earliest"),
),
},
},
})
}
@@ -0,0 +1,89 @@
package provider
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
)
var (
_ datasource.DataSource = &notificationTargetDataSource{}
_ datasource.DataSourceWithConfigure = &notificationTargetDataSource{}
)
func newNotificationTargetDataSource() datasource.DataSource {
return &notificationTargetDataSource{}
}
// notificationTargetDataSource looks up an existing notification target
// by ID -- see dashboardDataSource's doc comment for why this reuses
// notificationTargetResourceModel/notificationTargetModelFromAPI rather
// than a parallel type.
type notificationTargetDataSource struct {
client *client
}
func (d *notificationTargetDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_notification_target"
}
func (d *notificationTargetDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Looks up an existing Sentry notification target by ID. See the sentry_notification_target resource for how one is created/managed.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Required: true,
Description: "Target ID to look up.",
},
"tenant_id": schema.StringAttribute{Computed: true},
"name": schema.StringAttribute{Computed: true},
"kind": schema.StringAttribute{Computed: true},
"webhook_url": schema.StringAttribute{Computed: true},
"payload_template": schema.StringAttribute{
Computed: true,
},
"headers": schema.StringAttribute{Computed: true},
"secret": schema.StringAttribute{
Computed: true,
Sensitive: true,
Description: "alerting's own GET /targets/{id} returns this unredacted (see the " +
"sentry_notification_target resource's schema doc comment) -- Sensitive here for the " +
"same reason, and the same state-file caveat applies.",
},
"created_by": schema.StringAttribute{Computed: true},
},
}
}
func (d *notificationTargetDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
data, ok := req.ProviderData.(*providerData)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *provider.providerData, got: %T. This is a provider bug -- please report it.", req.ProviderData),
)
return
}
d.client = data.alerting
}
func (d *notificationTargetDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config notificationTargetResourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
out, err := d.client.getNotificationTarget(ctx, config.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Reading Notification Target", err.Error())
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, notificationTargetModelFromAPI(out))...)
}
@@ -0,0 +1,45 @@
package provider
import (
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)
// Same skip-gated-not-faked posture as
// TestAccNotificationTargetResource_basic -- see that test's doc
// comment.
func TestAccNotificationTargetDataSource_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: `
provider "sentry" {
endpoint = "http://localhost:8080"
alerting_endpoint = "http://localhost:8081"
}
resource "sentry_notification_target" "test" {
name = "Data Source Test Target"
kind = "webhook"
webhook_url = "https://example.com/hook"
secret = "test-secret"
}
data "sentry_notification_target" "test" {
id = sentry_notification_target.test.id
}
`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrPair("data.sentry_notification_target.test", "name", "sentry_notification_target.test", "name"),
resource.TestCheckResourceAttrPair("data.sentry_notification_target.test", "webhook_url", "sentry_notification_target.test", "webhook_url"),
// Real, not papered over -- secret round-trips
// unredacted (see the resource test's equivalent
// comment).
resource.TestCheckResourceAttrPair("data.sentry_notification_target.test", "secret", "sentry_notification_target.test", "secret"),
),
},
},
})
}
+5 -1
View File
@@ -135,5 +135,9 @@ func (p *sentryProvider) Resources(_ context.Context) []func() resource.Resource
} }
func (p *sentryProvider) DataSources(_ context.Context) []func() datasource.DataSource { func (p *sentryProvider) DataSources(_ context.Context) []func() datasource.DataSource {
return nil return []func() datasource.DataSource{
newDashboardDataSource,
newAlertRuleDataSource,
newNotificationTargetDataSource,
}
} }
@@ -4,6 +4,7 @@ import (
"context" "context"
"testing" "testing"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource"
) )
@@ -133,3 +134,75 @@ func TestNotificationTargetResourceMetadataSetsTypeName(t *testing.T) {
t.Fatalf("TypeName = %q, want sentry_notification_target", resp.TypeName) t.Fatalf("TypeName = %q, want sentry_notification_target", resp.TypeName)
} }
} }
func TestDashboardDataSourceSchemaValid(t *testing.T) {
ctx := context.Background()
req := datasource.SchemaRequest{}
resp := &datasource.SchemaResponse{}
newDashboardDataSource().Schema(ctx, req, resp)
if resp.Diagnostics.HasError() {
t.Fatalf("sentry_dashboard data source schema has errors: %v", resp.Diagnostics)
}
if !resp.Schema.Attributes["id"].IsRequired() {
t.Error(`"id" must be Required -- a data source needs it to know what to look up`)
}
if !resp.Schema.Attributes["name"].IsComputed() {
t.Error(`"name" must be Computed`)
}
}
func TestAlertRuleDataSourceSchemaValid(t *testing.T) {
ctx := context.Background()
req := datasource.SchemaRequest{}
resp := &datasource.SchemaResponse{}
newAlertRuleDataSource().Schema(ctx, req, resp)
if resp.Diagnostics.HasError() {
t.Fatalf("sentry_alert_rule data source schema has errors: %v", resp.Diagnostics)
}
if !resp.Schema.Attributes["id"].IsRequired() {
t.Error(`"id" must be Required`)
}
if !resp.Schema.Attributes["query"].IsComputed() {
t.Error(`"query" must be Computed`)
}
}
func TestNotificationTargetDataSourceSchemaValid(t *testing.T) {
ctx := context.Background()
req := datasource.SchemaRequest{}
resp := &datasource.SchemaResponse{}
newNotificationTargetDataSource().Schema(ctx, req, resp)
if resp.Diagnostics.HasError() {
t.Fatalf("sentry_notification_target data source schema has errors: %v", resp.Diagnostics)
}
if !resp.Schema.Attributes["id"].IsRequired() {
t.Error(`"id" must be Required`)
}
if !resp.Schema.Attributes["secret"].IsSensitive() {
t.Error(`"secret" must be Sensitive`)
}
}
func TestDataSourcesMetadataSetTypeNames(t *testing.T) {
cases := []struct {
newDS func() datasource.DataSource
wantType string
}{
{newDashboardDataSource, "sentry_dashboard"},
{newAlertRuleDataSource, "sentry_alert_rule"},
{newNotificationTargetDataSource, "sentry_notification_target"},
}
for _, c := range cases {
resp := &datasource.MetadataResponse{}
c.newDS().Metadata(context.Background(), datasource.MetadataRequest{ProviderTypeName: "sentry"}, resp)
if resp.TypeName != c.wantType {
t.Errorf("TypeName = %q, want %q", resp.TypeName, c.wantType)
}
}
}