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.
140 lines
5.1 KiB
Go
140 lines
5.1 KiB
Go
// Package provider is Sentry's Terraform provider implementation,
|
|
// built on HashiCorp's terraform-plugin-framework (not the legacy
|
|
// SDKv2 -- the framework is the actively-developed, currently-
|
|
// recommended library for a provider started from scratch, matching
|
|
// CLAUDE.md's "prefer boring, well-understood dependencies" read
|
|
// forward rather than backward).
|
|
package provider
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
|
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
|
"github.com/hashicorp/terraform-plugin-framework/provider"
|
|
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
)
|
|
|
|
var _ provider.Provider = &sentryProvider{}
|
|
|
|
// New matches providerserver.Serve's expected constructor shape --
|
|
// version is threaded through from main.go's -ldflags-injected build
|
|
// version.
|
|
func New(version string) func() provider.Provider {
|
|
return func() provider.Provider {
|
|
return &sentryProvider{version: version}
|
|
}
|
|
}
|
|
|
|
type sentryProvider struct {
|
|
version string
|
|
}
|
|
|
|
type sentryProviderModel struct {
|
|
Endpoint types.String `tfsdk:"endpoint"`
|
|
AlertingEndpoint types.String `tfsdk:"alerting_endpoint"`
|
|
Token types.String `tfsdk:"token"`
|
|
}
|
|
|
|
// providerData is what Configure hands resources/data sources via
|
|
// req.ProviderData -- two separate clients, not one, because `alerting`
|
|
// is a genuinely separate service with its own base URL (its own
|
|
// REST API, its own port, sometimes its own deployment) -- same split
|
|
// web/src/lib/api.ts's apiBase/alertingBase and cli/cmd/sentryctl's
|
|
// --api/--alerting-api already draw, not something invented for this
|
|
// provider.
|
|
type providerData struct {
|
|
api *client
|
|
alerting *client
|
|
}
|
|
|
|
func (p *sentryProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
|
|
resp.TypeName = "sentry"
|
|
resp.Version = p.version
|
|
}
|
|
|
|
func (p *sentryProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
|
|
resp.Schema = schema.Schema{
|
|
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,
|
|
Description: "Base URL of the api service, e.g. \"http://localhost:8080\". Defaults to " +
|
|
"$SENTRY_API_ENDPOINT, or \"http://localhost:8080\" if that's unset too -- same " +
|
|
"default sentryctl's --api/$SENTRYCTL_API_URL uses (cli/cmd/sentryctl/main.go).",
|
|
},
|
|
"alerting_endpoint": schema.StringAttribute{
|
|
Optional: true,
|
|
Description: "Base URL of the alerting service, e.g. \"http://localhost:8081\" -- a " +
|
|
"separate service from api, not a path under endpoint above (see " +
|
|
"/docs/phase-3-alerting-design.md's component boundary). Defaults to " +
|
|
"$SENTRY_ALERTING_API_ENDPOINT, or \"http://localhost:8081\" if that's unset too -- " +
|
|
"same default sentryctl's --alerting-api/$SENTRYCTL_ALERTING_API_URL uses.",
|
|
},
|
|
"token": schema.StringAttribute{
|
|
Optional: true,
|
|
Sensitive: true,
|
|
Description: "Bearer credential sent as \"Authorization: Bearer <token>\" on every request " +
|
|
"-- required once a deployment configures enterprise-auth (see " +
|
|
"/docs/phase-4-rbac-design.md), same as sentryctl's $SENTRYCTL_TOKEN. Defaults to " +
|
|
"$SENTRY_API_TOKEN if unset. Set via a variable or environment, never a literal in a " +
|
|
".tf file committed to version control.",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Configure resolves endpoint/token the same precedence order
|
|
// sentryctl's resolveAPIURL/resolveToken use (explicit config value,
|
|
// then an environment variable, then a hardcoded default) so behavior
|
|
// stays predictable across both of this project's Sentry API clients.
|
|
func (p *sentryProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
|
|
var config sentryProviderModel
|
|
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
endpoint := config.Endpoint.ValueString()
|
|
if endpoint == "" {
|
|
endpoint = os.Getenv("SENTRY_API_ENDPOINT")
|
|
}
|
|
if endpoint == "" {
|
|
endpoint = "http://localhost:8080"
|
|
}
|
|
|
|
alertingEndpoint := config.AlertingEndpoint.ValueString()
|
|
if alertingEndpoint == "" {
|
|
alertingEndpoint = os.Getenv("SENTRY_ALERTING_API_ENDPOINT")
|
|
}
|
|
if alertingEndpoint == "" {
|
|
alertingEndpoint = "http://localhost:8081"
|
|
}
|
|
|
|
token := config.Token.ValueString()
|
|
if token == "" {
|
|
token = os.Getenv("SENTRY_API_TOKEN")
|
|
}
|
|
|
|
data := &providerData{
|
|
api: newClient(endpoint, token),
|
|
alerting: newClient(alertingEndpoint, token),
|
|
}
|
|
resp.DataSourceData = data
|
|
resp.ResourceData = data
|
|
}
|
|
|
|
func (p *sentryProvider) Resources(_ context.Context) []func() resource.Resource {
|
|
return []func() resource.Resource{
|
|
newDashboardResource,
|
|
newAlertRuleResource,
|
|
newNotificationTargetResource,
|
|
}
|
|
}
|
|
|
|
func (p *sentryProvider) DataSources(_ context.Context) []func() datasource.DataSource {
|
|
return nil
|
|
}
|