CLAUDE.md names the Terraform provider a first-class deliverable
alongside sentryctl ("CLI and Terraform provider are first-class, not
afterthoughts"), but no phase before this one had actually built any of
it -- no terraform/ directory existed. This is a first slice, not a
finished provider: one resource, scoped and confirmed with the project
owner before starting (a new pinned external dependency and an
architectural decision not covered in /docs/architecture.md are both
things CLAUDE.md's own "When in doubt" section says to ask about).
New Go module (terraform/, github.com/sentry/sentry/terraform) built on
HashiCorp's terraform-plugin-framework -- the actively-developed
library, not the legacy SDKv2, since there's no existing provider code
to migrate and no reason to start new on the framework HashiCorp itself
steers people away from.
internal/provider/client.go talks the exact same JSON contract
sentryctl's "dashboards apply" and web's Export JSON button already use
against api/dashboards.Handler (POST/GET/PUT/DELETE /dashboards[/{id}]) --
cli/README.md already named this "the seed of a future Terraform
provider: one JSON contract, multiple callers," this is that third
caller, not a new contract invented for Terraform's sake.
sentry_dashboard's schema deliberately leaves default_earliest/
default_latest Optional+Computed with no Terraform-side static default,
even though the API defaults them to "-1h"/"now" when empty -- letting
the API stay the one source of truth for what "unset" means rather than
duplicating that default in two places that could drift. tenant_id is
Computed-only, matching api/dashboards.Handler's own tenantID() doc
comment that a client-supplied value is always overridden server-side.
Panels are not modeled by this resource -- a genuinely separate resource
shape (own lifecycle, own endpoints, own validation needs), scoped out
deliberately, not an oversight. Alert rules, notification targets, and
tenant/RBAC resources are the same: real, disclosed future work, not
attempted in this pass. See terraform/README.md for the full accounting.
Verified: client_test.go runs real HTTP round trips against httptest.
Server (request construction, response parsing, the 404-vs-other-error
distinction Read/Delete need for Terraform's out-of-band-deletion
convention) -- same pattern cli/cmd/sentryctl's own tests already use
against the same api/dashboards endpoints. provider_test.go validates
both schemas are internally well-formed without needing a Terraform
binary. dashboard_resource_test.go's TestAccDashboardResource_basic is a
real acceptance test (terraform-plugin-testing), skip-gated by TF_ACC=1
per that framework's own convention -- even with TF_ACC set it would
still need a live api service (Postgres+ClickHouse) to apply against,
which this environment has no Docker access to bring up, so it has not
actually run here, same disclosed gap as every other live-infra test in
this repo.
67 lines
2.1 KiB
Go
67 lines
2.1 KiB
Go
package provider
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/terraform-plugin-framework/provider"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
|
)
|
|
|
|
// TestProviderSchemaValid and TestDashboardResourceSchemaValid don't
|
|
// need a Terraform binary or a live api service -- ValidateImplementation
|
|
// runs the same internal consistency checks
|
|
// terraform-plugin-framework's own protocol layer would (attribute
|
|
// names are valid identifiers, no Optional+Required conflicts, etc.),
|
|
// catching a broken schema before it ever reaches an acceptance test.
|
|
func TestProviderSchemaValid(t *testing.T) {
|
|
ctx := context.Background()
|
|
req := provider.SchemaRequest{}
|
|
resp := &provider.SchemaResponse{}
|
|
|
|
New("test")().Schema(ctx, req, resp)
|
|
|
|
if resp.Diagnostics.HasError() {
|
|
t.Fatalf("provider schema has errors: %v", resp.Diagnostics)
|
|
}
|
|
for _, attr := range []string{"endpoint", "token"} {
|
|
if _, ok := resp.Schema.Attributes[attr]; !ok {
|
|
t.Errorf("provider schema missing expected attribute %q", attr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDashboardResourceSchemaValid(t *testing.T) {
|
|
ctx := context.Background()
|
|
req := resource.SchemaRequest{}
|
|
resp := &resource.SchemaResponse{}
|
|
|
|
newDashboardResource().Schema(ctx, req, resp)
|
|
|
|
if resp.Diagnostics.HasError() {
|
|
t.Fatalf("sentry_dashboard schema has errors: %v", resp.Diagnostics)
|
|
}
|
|
for _, attr := range []string{
|
|
"id", "tenant_id", "name", "description",
|
|
"default_earliest", "default_latest", "created_by", "created_at", "updated_at",
|
|
} {
|
|
if _, ok := resp.Schema.Attributes[attr]; !ok {
|
|
t.Errorf("sentry_dashboard schema missing expected attribute %q", attr)
|
|
}
|
|
}
|
|
if !resp.Schema.Attributes["name"].IsRequired() {
|
|
t.Error(`"name" must be Required`)
|
|
}
|
|
if !resp.Schema.Attributes["id"].IsComputed() {
|
|
t.Error(`"id" must be Computed`)
|
|
}
|
|
}
|
|
|
|
func TestDashboardResourceMetadataSetsTypeName(t *testing.T) {
|
|
resp := &resource.MetadataResponse{}
|
|
newDashboardResource().Metadata(context.Background(), resource.MetadataRequest{ProviderTypeName: "sentry"}, resp)
|
|
if resp.TypeName != "sentry_dashboard" {
|
|
t.Fatalf("TypeName = %q, want sentry_dashboard", resp.TypeName)
|
|
}
|
|
}
|