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:
@@ -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
@@ -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 = ¬ificationTargetDataSource{}
|
||||||
|
_ datasource.DataSourceWithConfigure = ¬ificationTargetDataSource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func newNotificationTargetDataSource() datasource.DataSource {
|
||||||
|
return ¬ificationTargetDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user