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:
@@ -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 {
|
||||
return nil
|
||||
return []func() datasource.DataSource{
|
||||
newDashboardDataSource,
|
||||
newAlertRuleDataSource,
|
||||
newNotificationTargetDataSource,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/provider"
|
||||
"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)
|
||||
}
|
||||
}
|
||||
|
||||
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