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.
84 lines
3.2 KiB
Go
84 lines
3.2 KiB
Go
package provider
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/hashicorp/terraform-plugin-framework/providerserver"
|
|
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
|
|
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
|
)
|
|
|
|
// testAccProtoV6ProviderFactories wires this package's own provider
|
|
// implementation into terraform-plugin-testing's acceptance-test
|
|
// runner -- HashiCorp's standard pattern, one factory reused by every
|
|
// acceptance test in this package.
|
|
var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){
|
|
"sentry": providerserver.NewProtocol6WithError(New("test")()),
|
|
}
|
|
|
|
// The acceptance test below is gated the same way every other live-
|
|
// infrastructure test in this repo is (skip-gated, not deleted or
|
|
// faked) -- terraform-plugin-testing's own resource.Test already skips
|
|
// unless TF_ACC=1 is set, the framework's standard convention, and it
|
|
// additionally needs a real running api service (Docker/Postgres this
|
|
// environment doesn't have access to -- see /docs/phase-4-runbook.md's
|
|
// "Verification status" section for the same disclosed gap everywhere
|
|
// else in this codebase). "The test exists and is correct Go" is not
|
|
// the same claim as "this resource has been applied for real," per this
|
|
// repo's established honesty discipline.
|
|
func TestAccDashboardResource_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 = "Acceptance Test Dashboard"
|
|
description = "created by TestAccDashboardResource_basic"
|
|
}
|
|
`,
|
|
Check: resource.ComposeAggregateTestCheckFunc(
|
|
resource.TestCheckResourceAttr("sentry_dashboard.test", "name", "Acceptance Test Dashboard"),
|
|
resource.TestCheckResourceAttr("sentry_dashboard.test", "description", "created by TestAccDashboardResource_basic"),
|
|
resource.TestCheckResourceAttrSet("sentry_dashboard.test", "id"),
|
|
resource.TestCheckResourceAttrSet("sentry_dashboard.test", "tenant_id"),
|
|
// Left unset in config -- must come back as the
|
|
// server's own defaults (store.go: "-1h"/"now"), not
|
|
// an empty string, proving the Optional+Computed
|
|
// schema round-trips the server's default rather
|
|
// than fighting it with a Terraform-side one.
|
|
resource.TestCheckResourceAttr("sentry_dashboard.test", "default_earliest", "-1h"),
|
|
resource.TestCheckResourceAttr("sentry_dashboard.test", "default_latest", "now"),
|
|
),
|
|
},
|
|
{
|
|
// Update: name change should apply in place, not
|
|
// replace (no RequiresReplace plan modifier on name).
|
|
Config: `
|
|
provider "sentry" {
|
|
endpoint = "http://localhost:8080"
|
|
}
|
|
|
|
resource "sentry_dashboard" "test" {
|
|
name = "Renamed Dashboard"
|
|
description = "created by TestAccDashboardResource_basic"
|
|
}
|
|
`,
|
|
Check: resource.TestCheckResourceAttr("sentry_dashboard.test", "name", "Renamed Dashboard"),
|
|
},
|
|
{
|
|
// Import: re-reads by ID alone and must match what's in
|
|
// state, proving Read()'s server round trip agrees with
|
|
// what Create()/Update() last wrote.
|
|
ResourceName: "sentry_dashboard.test",
|
|
ImportState: true,
|
|
ImportStateVerify: true,
|
|
},
|
|
},
|
|
})
|
|
}
|