The open design question named in the last three commits' README --
"a sentry_dashboard_panel resource (or a panels list block on this one)"
-- is resolved: separate resource, matching api/dashboards.Handler's own
shape (a panel is created/updated/deleted independently of its parent
dashboard via its own endpoints, never by rewriting the dashboard's
whole panel list). A nested list block would have forced every panel to
be rewritten on any single panel's change, hiding fine-grained diffs a
separate resource shows naturally -- the more idiomatic Terraform
pattern for independently-lifecycled child resources, and the one that
matches what the API actually does.
Unlike sentry_alert_rule/sentry_notification_target, this resource
supports a real in-place Update -- api/dashboards.Handler actually has a
PUT /dashboards/{id}/panels/{panelId}. Only dashboard_id forces
RequiresReplace: UpdatePanel's SQL matches WHERE id = $panelID AND
dashboard_id = $dashboardID, so changing dashboard_id through the
existing panel's URL wouldn't move it, it would just fail to match --
there's no API operation for "move a panel to a different dashboard."
Panels have no standalone GET endpoint -- only GET /dashboards/{id},
which includes the full panels array. client.go's new getPanel fetches
the parent dashboard and finds the panel by ID within it, returning the
same *apiError{StatusCode: 404} shape a direct GET would whether the
dashboard itself or just the panel within it is gone, so isNotFound
works identically either way. This also means a bare panel ID isn't
enough to import from -- ImportState takes "dashboard_id/panel_id" and
splits on the last "/", the one resource here with a composite import
identifier.
query_language never accepts "sql" for panels specifically -- confirmed
in api/dashboards's own validatePanel ("dashboards only support
pipe-syntax queries, since the dashboard time-range picker is injected
as leading query terms"), a real constraint from the API this client
doesn't re-validate client-side (same "let the API be the one source of
truth for validation" posture the other resources already take), but
documented in the schema so it's not a surprise 400 from Create.
sentry_dashboard_panel gets a matching data source too
(dashboard_id + id both Required, unlike the other three data sources'
single Required id, since getPanel itself needs both).
Verified: client tests are real httptest.Server round trips, including
getPanel finding the right panel within a real dashboard response and
returning a recognizable not-found both when the panel is missing and
when the parent dashboard itself is gone. Schema validation needs no
Terraform binary. TestAccDashboardPanelResource_basic and
TestAccDashboardPanelDataSource_basic are real acceptance tests,
skip-gated by TF_ACC same as the other six -- the resource test proves a
genuine in-place update (a title change, no plancheck needed since
in-place update is the default expectation here, unlike the
create/destroy-only resources). Not run against a live stack in this
environment, same disclosed gap as everything else Docker-gated in this
repo.
146 lines
5.3 KiB
Go
146 lines
5.3 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,
|
|
newDashboardPanelResource,
|
|
newAlertRuleResource,
|
|
newNotificationTargetResource,
|
|
}
|
|
}
|
|
|
|
func (p *sentryProvider) DataSources(_ context.Context) []func() datasource.DataSource {
|
|
return []func() datasource.DataSource{
|
|
newDashboardDataSource,
|
|
newDashboardPanelDataSource,
|
|
newAlertRuleDataSource,
|
|
newNotificationTargetDataSource,
|
|
}
|
|
}
|