Add sentry_notification_target, closing the alert-rule-as-code loop
sentry_alert_rule.notification_target_id could previously only point at
a target created outside Terraform (sentryctl/curl/the web UI) --
without this resource, "manage alert rules as code" was only half true.
Same create/destroy-only shape as sentry_alert_rule and for the same
reason: alerting has no PUT /targets/{id} either, confirmed down to
notifystore.Store (Create/List/Get/Delete, no Update).
client.go's notificationTarget type mirrors notifystore.Target's JSON
shape. headers stays raw JSON bytes end to end -- the client has no
opinion about its shape (neither does alerting's own Target type,
json.RawMessage), and the resource layer round-trips it as a plain
JSON-text string a caller provides via Terraform's jsonencode().
secret is marked Sensitive in the schema, but alerting's own
GET /targets/{id} returns it unredacted (confirmed in
notifystore/store.go -- no redaction at the store or handler layer, an
existing property of alerting's API, not something this provider
introduces). A new client test
(TestGetNotificationTargetReturnsSecretUnredacted) documents that real
behavior so a future change to it would be caught here, not discovered
by surprise. Sensitive keeps the value out of plan/apply console output;
it does not keep it out of Terraform state, the standard caveat for any
sensitive attribute, named explicitly in the schema description and
README rather than left implicit.
Examples updated end to end: sentry_alert_rule's example now creates a
real sentry_notification_target and references its .id, instead of a
placeholder string.
Verified: client tests are real httptest.Server round trips. Schema
validation needs no Terraform binary.
TestAccNotificationTargetResource_basic is a real acceptance test,
skip-gated by TF_ACC same as the other two, including a
plancheck.ExpectResourceAction assertion that a config change actually
plans destroy-then-create, and (since secret really does round-trip
unredacted) a real ImportStateVerify on the secret attribute rather than
one papered over with ImportStateVerifyIgnore. Not run against a live
stack in this environment, same disclosed gap as everything else
Docker-gated in this repo.
This commit is contained in:
@@ -200,3 +200,58 @@ func (c *client) getRule(ctx context.Context, id string) (*rule, error) {
|
||||
func (c *client) deleteRule(ctx context.Context, id string) error {
|
||||
return c.do(ctx, http.MethodDelete, "/rules/"+id, nil, nil)
|
||||
}
|
||||
|
||||
// notificationTarget mirrors alerting/internal/notifystore.Target's
|
||||
// JSON shape -- deliberately a local type, same "talk HTTP, not Go
|
||||
// imports" posture as dashboard/rule above. Headers is left as raw
|
||||
// JSON bytes (not decoded into a Go map) since this client has no
|
||||
// opinion about its shape -- alerting's own Target type doesn't either
|
||||
// (json.RawMessage), and the resource layer round-trips it as a plain
|
||||
// JSON-text string a caller provides via Terraform's jsonencode().
|
||||
//
|
||||
// Secret genuinely comes back from GET/List unredacted -- confirmed in
|
||||
// notifystore/store.go's Get/List queries, which select the secret
|
||||
// column with no redaction at either the store or handler layer. This
|
||||
// is alerting's own existing behavior, not something this provider
|
||||
// introduces or could fix from the client side; notificationTargetResource
|
||||
// marks the corresponding attribute Sensitive so Terraform at least
|
||||
// doesn't print it in plan/apply console output (it is still stored in
|
||||
// Terraform state in plaintext -- a standard, disclosed Terraform
|
||||
// limitation for any sensitive attribute, not specific to this one).
|
||||
type notificationTarget struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
TenantID string `json:"tenant_id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Kind string `json:"kind"`
|
||||
WebhookURL string `json:"webhook_url"`
|
||||
PayloadTemplate *string `json:"payload_template,omitempty"`
|
||||
Headers json.RawMessage `json:"headers,omitempty"`
|
||||
Secret *string `json:"secret,omitempty"`
|
||||
CreatedBy string `json:"created_by,omitempty"`
|
||||
}
|
||||
|
||||
// createNotificationTarget and getNotificationTarget are the only
|
||||
// mutating/reading calls this client makes against /targets -- same
|
||||
// "no updateX method, alerting has no PUT /targets/{id} either" shape
|
||||
// as rules above (rulestore.Store/notifystore.Store both only have
|
||||
// Create/List/Get/Delete). notificationTargetResource is create/destroy
|
||||
// only for the same reason alertRuleResource is.
|
||||
func (c *client) createNotificationTarget(ctx context.Context, t *notificationTarget) (*notificationTarget, error) {
|
||||
var out notificationTarget
|
||||
if err := c.do(ctx, http.MethodPost, "/targets", t, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *client) getNotificationTarget(ctx context.Context, id string) (*notificationTarget, error) {
|
||||
var out notificationTarget
|
||||
if err := c.do(ctx, http.MethodGet, "/targets/"+id, nil, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *client) deleteNotificationTarget(ctx context.Context, id string) error {
|
||||
return c.do(ctx, http.MethodDelete, "/targets/"+id, nil, nil)
|
||||
}
|
||||
|
||||
@@ -241,3 +241,78 @@ func TestDeleteRuleSendsToCorrectPath(t *testing.T) {
|
||||
t.Fatal("expected the server to receive a DELETE request")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateNotificationTargetSendsExpectedRequest(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/targets" {
|
||||
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
var body notificationTarget
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decoding request body: %v", err)
|
||||
}
|
||||
if body.Name != "Ops Webhook" || body.Kind != "webhook" {
|
||||
t.Errorf("unexpected request body: %+v", body)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(notificationTarget{
|
||||
ID: "target-1", TenantID: "acme", Name: body.Name, Kind: body.Kind, WebhookURL: body.WebhookURL,
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := newClient(srv.URL, "")
|
||||
out, err := c.createNotificationTarget(context.Background(), ¬ificationTarget{
|
||||
Name: "Ops Webhook", Kind: "webhook", WebhookURL: "https://example.com/hook",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("createNotificationTarget: %v", err)
|
||||
}
|
||||
if out.ID != "target-1" {
|
||||
t.Fatalf("unexpected response: %+v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNotificationTargetReturnsSecretUnredacted(t *testing.T) {
|
||||
// Documents real, existing alerting behavior (notifystore's Get
|
||||
// query selects the secret column with no redaction) -- this test
|
||||
// exists so a future change to alerting's redaction posture would
|
||||
// be caught here too, not just discovered by surprise.
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(notificationTarget{ID: "target-1", Name: "Ops Webhook", Kind: "webhook", Secret: strPtr("shh")})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := newClient(srv.URL, "")
|
||||
out, err := c.getNotificationTarget(context.Background(), "target-1")
|
||||
if err != nil {
|
||||
t.Fatalf("getNotificationTarget: %v", err)
|
||||
}
|
||||
if out.Secret == nil || *out.Secret != "shh" {
|
||||
t.Fatalf("Secret = %v, want it echoed back unredacted", out.Secret)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteNotificationTargetSendsToCorrectPath(t *testing.T) {
|
||||
called := false
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
if r.Method != http.MethodDelete || r.URL.Path != "/targets/target-1" {
|
||||
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := newClient(srv.URL, "")
|
||||
if err := c.deleteNotificationTarget(context.Background(), "target-1"); err != nil {
|
||||
t.Fatalf("deleteNotificationTarget: %v", err)
|
||||
}
|
||||
if !called {
|
||||
t.Fatal("expected the server to receive a DELETE request")
|
||||
}
|
||||
}
|
||||
|
||||
func strPtr(s string) *string { return &s }
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"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/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = ¬ificationTargetResource{}
|
||||
_ resource.ResourceWithConfigure = ¬ificationTargetResource{}
|
||||
_ resource.ResourceWithImportState = ¬ificationTargetResource{}
|
||||
)
|
||||
|
||||
func newNotificationTargetResource() resource.Resource {
|
||||
return ¬ificationTargetResource{}
|
||||
}
|
||||
|
||||
// notificationTargetResource implements sentry_notification_target
|
||||
// against alerting/internal/httpapi's POST/GET/DELETE
|
||||
// /targets[/{id}] endpoints.
|
||||
//
|
||||
// Deliberately create/destroy only, every attribute RequiresReplace --
|
||||
// same reasoning as alertRuleResource (see that file's doc comment):
|
||||
// alerting has no PUT /targets/{id} at all, confirmed down to
|
||||
// notifystore.Store, which has Create/List/Get/Delete but no Update.
|
||||
type notificationTargetResource struct {
|
||||
client *client
|
||||
}
|
||||
|
||||
type notificationTargetResourceModel struct {
|
||||
ID types.String `tfsdk:"id"`
|
||||
TenantID types.String `tfsdk:"tenant_id"`
|
||||
Name types.String `tfsdk:"name"`
|
||||
Kind types.String `tfsdk:"kind"`
|
||||
WebhookURL types.String `tfsdk:"webhook_url"`
|
||||
PayloadTemplate types.String `tfsdk:"payload_template"`
|
||||
Headers types.String `tfsdk:"headers"`
|
||||
Secret types.String `tfsdk:"secret"`
|
||||
CreatedBy types.String `tfsdk:"created_by"`
|
||||
}
|
||||
|
||||
func (r *notificationTargetResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_notification_target"
|
||||
}
|
||||
|
||||
func (r *notificationTargetResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
replace := []planmodifier.String{stringplanmodifier.RequiresReplace()}
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "A Sentry alert notification target. Create/destroy only -- alerting has no update " +
|
||||
"endpoint for targets today (see this resource's Go doc comment), so every attribute below " +
|
||||
"forces a destroy-and-recreate on change, never an in-place update.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
|
||||
Description: "Server-generated target ID -- reference this from a sentry_alert_rule's notification_target_id.",
|
||||
},
|
||||
"tenant_id": schema.StringAttribute{
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
|
||||
},
|
||||
"name": schema.StringAttribute{
|
||||
Required: true,
|
||||
PlanModifiers: replace,
|
||||
Description: "Target name. The API rejects an empty string.",
|
||||
},
|
||||
"kind": schema.StringAttribute{
|
||||
Required: true,
|
||||
PlanModifiers: replace,
|
||||
Description: `One of "webhook", "slack", "pagerduty".`,
|
||||
},
|
||||
"webhook_url": schema.StringAttribute{
|
||||
Required: true,
|
||||
PlanModifiers: replace,
|
||||
Description: "Destination URL. The API rejects an empty string, regardless of kind.",
|
||||
},
|
||||
"payload_template": schema.StringAttribute{
|
||||
Optional: true,
|
||||
PlanModifiers: replace,
|
||||
Description: "Optional Go text/template string overriding the default payload shape for this target's kind.",
|
||||
},
|
||||
"headers": schema.StringAttribute{
|
||||
Optional: true,
|
||||
PlanModifiers: replace,
|
||||
Description: `Optional extra HTTP headers, as a JSON object string -- e.g. jsonencode({"X-Custom" = "value"}). Stored and returned as opaque JSON; this provider does not interpret it.`,
|
||||
},
|
||||
"secret": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Sensitive: true,
|
||||
PlanModifiers: replace,
|
||||
Description: "Optional shared secret (e.g. for HMAC-signing outgoing webhook payloads). " +
|
||||
"alerting's GET /targets/{id} returns this back unredacted (confirmed in " +
|
||||
"notifystore/store.go -- no redaction at the store or handler layer), so it is " +
|
||||
"necessarily present in this resource's Terraform state in plaintext, the standard " +
|
||||
"caveat for any Sensitive Terraform attribute: treat state files as sensitive, " +
|
||||
"encrypt the backend, restrict who can read them.",
|
||||
},
|
||||
"created_by": schema.StringAttribute{
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *notificationTargetResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
data, ok := req.ProviderData.(*providerData)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected *provider.providerData, got: %T. This is a provider bug -- please report it.", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
r.client = data.alerting
|
||||
}
|
||||
|
||||
func notificationTargetModelFromAPI(t *notificationTarget) notificationTargetResourceModel {
|
||||
m := notificationTargetResourceModel{
|
||||
ID: types.StringValue(t.ID),
|
||||
TenantID: types.StringValue(t.TenantID),
|
||||
Name: types.StringValue(t.Name),
|
||||
Kind: types.StringValue(t.Kind),
|
||||
WebhookURL: types.StringValue(t.WebhookURL),
|
||||
CreatedBy: types.StringValue(t.CreatedBy),
|
||||
}
|
||||
if t.PayloadTemplate != nil {
|
||||
m.PayloadTemplate = types.StringValue(*t.PayloadTemplate)
|
||||
}
|
||||
if len(t.Headers) > 0 {
|
||||
m.Headers = types.StringValue(string(t.Headers))
|
||||
}
|
||||
if t.Secret != nil {
|
||||
m.Secret = types.StringValue(*t.Secret)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func notificationTargetAPIFromModel(m notificationTargetResourceModel) (*notificationTarget, error) {
|
||||
t := ¬ificationTarget{
|
||||
Name: m.Name.ValueString(),
|
||||
Kind: m.Kind.ValueString(),
|
||||
WebhookURL: m.WebhookURL.ValueString(),
|
||||
}
|
||||
if !m.PayloadTemplate.IsNull() {
|
||||
v := m.PayloadTemplate.ValueString()
|
||||
t.PayloadTemplate = &v
|
||||
}
|
||||
if !m.Headers.IsNull() {
|
||||
raw := m.Headers.ValueString()
|
||||
if !json.Valid([]byte(raw)) {
|
||||
return nil, fmt.Errorf("headers must be valid JSON (use jsonencode(...) in the resource config), got: %s", raw)
|
||||
}
|
||||
t.Headers = json.RawMessage(raw)
|
||||
}
|
||||
if !m.Secret.IsNull() {
|
||||
v := m.Secret.ValueString()
|
||||
t.Secret = &v
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (r *notificationTargetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan notificationTargetResourceModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
in, err := notificationTargetAPIFromModel(plan)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Invalid Configuration", err.Error())
|
||||
return
|
||||
}
|
||||
out, err := r.client.createNotificationTarget(ctx, in)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Creating Notification Target", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, notificationTargetModelFromAPI(out))...)
|
||||
}
|
||||
|
||||
func (r *notificationTargetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state notificationTargetResourceModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
out, err := r.client.getNotificationTarget(ctx, state.ID.ValueString())
|
||||
if err != nil {
|
||||
if isNotFound(err) {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.AddError("Reading Notification Target", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, notificationTargetModelFromAPI(out))...)
|
||||
}
|
||||
|
||||
// Update should be unreachable in practice -- see alertRuleResource's
|
||||
// Update doc comment for why this is a safe read-only passthrough
|
||||
// rather than calling any mutating endpoint (alerting has none to call).
|
||||
func (r *notificationTargetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan notificationTargetResourceModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
out, err := r.client.getNotificationTarget(ctx, plan.ID.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Reading Notification Target", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, notificationTargetModelFromAPI(out))...)
|
||||
}
|
||||
|
||||
func (r *notificationTargetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state notificationTargetResourceModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.client.deleteNotificationTarget(ctx, state.ID.ValueString()); err != nil && !isNotFound(err) {
|
||||
resp.Diagnostics.AddError("Deleting Notification Target", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (r *notificationTargetResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
||||
"github.com/hashicorp/terraform-plugin-testing/plancheck"
|
||||
)
|
||||
|
||||
// Same skip-gated-not-faked posture as TestAccDashboardResource_basic /
|
||||
// TestAccAlertRuleResource_basic -- see those tests' doc comments.
|
||||
func TestAccNotificationTargetResource_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 = "Acceptance Test Target"
|
||||
kind = "webhook"
|
||||
webhook_url = "https://example.com/hook"
|
||||
secret = "test-secret"
|
||||
}
|
||||
`,
|
||||
Check: resource.ComposeAggregateTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("sentry_notification_target.test", "name", "Acceptance Test Target"),
|
||||
resource.TestCheckResourceAttr("sentry_notification_target.test", "kind", "webhook"),
|
||||
resource.TestCheckResourceAttr("sentry_notification_target.test", "secret", "test-secret"),
|
||||
resource.TestCheckResourceAttrSet("sentry_notification_target.test", "id"),
|
||||
resource.TestCheckResourceAttrSet("sentry_notification_target.test", "tenant_id"),
|
||||
),
|
||||
},
|
||||
{
|
||||
// Same create/destroy-only proof as
|
||||
// TestAccAlertRuleResource_basic's second step --
|
||||
// notifystore.Store has no Update either.
|
||||
Config: `
|
||||
provider "sentry" {
|
||||
endpoint = "http://localhost:8080"
|
||||
alerting_endpoint = "http://localhost:8081"
|
||||
}
|
||||
|
||||
resource "sentry_notification_target" "test" {
|
||||
name = "Renamed Target"
|
||||
kind = "webhook"
|
||||
webhook_url = "https://example.com/hook"
|
||||
secret = "test-secret"
|
||||
}
|
||||
`,
|
||||
ConfigPlanChecks: resource.ConfigPlanChecks{
|
||||
PreApply: []plancheck.PlanCheck{
|
||||
plancheck.ExpectResourceAction("sentry_notification_target.test", plancheck.ResourceActionDestroyBeforeCreate),
|
||||
},
|
||||
},
|
||||
Check: resource.TestCheckResourceAttr("sentry_notification_target.test", "name", "Renamed Target"),
|
||||
},
|
||||
{
|
||||
// No ImportStateVerifyIgnore for "secret" -- GET
|
||||
// really does return it unredacted (see client.go's
|
||||
// doc comment and TestGetNotificationTargetReturnsSecretUnredacted),
|
||||
// so import-time equality is a real, meaningful
|
||||
// assertion here, not one this test has to paper over.
|
||||
ResourceName: "sentry_notification_target.test",
|
||||
ImportState: true,
|
||||
ImportStateVerify: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -57,7 +57,7 @@ func (p *sentryProvider) Metadata(_ context.Context, _ provider.MetadataRequest,
|
||||
|
||||
func (p *sentryProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Manages Sentry log-aggregation-platform resources. Dashboards and alert rules for now -- notification targets and tenant/RBAC resources are real, disclosed future work, not built in this pass; see the provider README.",
|
||||
Description: "Manages Sentry log-aggregation-platform resources. Dashboards, alert rules, and notification targets for now -- tenant/RBAC resources are real, disclosed future work, not built in this pass; see the provider README.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"endpoint": schema.StringAttribute{
|
||||
Optional: true,
|
||||
@@ -130,6 +130,7 @@ func (p *sentryProvider) Resources(_ context.Context) []func() resource.Resource
|
||||
return []func() resource.Resource{
|
||||
newDashboardResource,
|
||||
newAlertRuleResource,
|
||||
newNotificationTargetResource,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -99,3 +99,37 @@ func TestAlertRuleResourceMetadataSetsTypeName(t *testing.T) {
|
||||
t.Fatalf("TypeName = %q, want sentry_alert_rule", resp.TypeName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotificationTargetResourceSchemaValid(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
req := resource.SchemaRequest{}
|
||||
resp := &resource.SchemaResponse{}
|
||||
|
||||
newNotificationTargetResource().Schema(ctx, req, resp)
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
t.Fatalf("sentry_notification_target schema has errors: %v", resp.Diagnostics)
|
||||
}
|
||||
for _, attr := range []string{
|
||||
"id", "tenant_id", "name", "kind", "webhook_url",
|
||||
"payload_template", "headers", "secret", "created_by",
|
||||
} {
|
||||
if _, ok := resp.Schema.Attributes[attr]; !ok {
|
||||
t.Errorf("sentry_notification_target schema missing expected attribute %q", attr)
|
||||
}
|
||||
}
|
||||
if !resp.Schema.Attributes["name"].IsRequired() {
|
||||
t.Error(`"name" must be Required`)
|
||||
}
|
||||
if !resp.Schema.Attributes["secret"].IsSensitive() {
|
||||
t.Error(`"secret" must be Sensitive`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotificationTargetResourceMetadataSetsTypeName(t *testing.T) {
|
||||
resp := &resource.MetadataResponse{}
|
||||
newNotificationTargetResource().Metadata(context.Background(), resource.MetadataRequest{ProviderTypeName: "sentry"}, resp)
|
||||
if resp.TypeName != "sentry_notification_target" {
|
||||
t.Fatalf("TypeName = %q, want sentry_notification_target", resp.TypeName)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user