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.
87 lines
2.8 KiB
Go
87 lines
2.8 KiB
Go
package provider
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
|
tfstate "github.com/hashicorp/terraform-plugin-testing/terraform"
|
|
)
|
|
|
|
// Same skip-gated-not-faked posture as TestAccDashboardResource_basic --
|
|
// see that test's doc comment.
|
|
func TestAccDashboardPanelResource_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 = "Panel Acceptance Test Dashboard"
|
|
}
|
|
|
|
resource "sentry_dashboard_panel" "test" {
|
|
dashboard_id = sentry_dashboard.test.id
|
|
title = "Errors over time"
|
|
query = "status>=500 | timechart count"
|
|
viz_type = "line"
|
|
}
|
|
`,
|
|
Check: resource.ComposeAggregateTestCheckFunc(
|
|
resource.TestCheckResourceAttrPair("sentry_dashboard_panel.test", "dashboard_id", "sentry_dashboard.test", "id"),
|
|
resource.TestCheckResourceAttr("sentry_dashboard_panel.test", "title", "Errors over time"),
|
|
resource.TestCheckResourceAttr("sentry_dashboard_panel.test", "viz_type", "line"),
|
|
resource.TestCheckResourceAttrSet("sentry_dashboard_panel.test", "id"),
|
|
// Left unset in config -- must come back as the
|
|
// API's own default ("{}"), same "API default, not
|
|
// a duplicated Terraform-side one" reasoning
|
|
// sentry_dashboard's default_earliest/default_latest
|
|
// use.
|
|
resource.TestCheckResourceAttr("sentry_dashboard_panel.test", "viz_config", "{}"),
|
|
),
|
|
},
|
|
{
|
|
// Update: unlike sentry_alert_rule/sentry_notification_target,
|
|
// this really is an in-place update -- api/dashboards.Handler
|
|
// has a real PUT for panels.
|
|
Config: `
|
|
provider "sentry" {
|
|
endpoint = "http://localhost:8080"
|
|
}
|
|
|
|
resource "sentry_dashboard" "test" {
|
|
name = "Panel Acceptance Test Dashboard"
|
|
}
|
|
|
|
resource "sentry_dashboard_panel" "test" {
|
|
dashboard_id = sentry_dashboard.test.id
|
|
title = "Errors over time (renamed)"
|
|
query = "status>=500 | timechart count"
|
|
viz_type = "line"
|
|
}
|
|
`,
|
|
Check: resource.TestCheckResourceAttr("sentry_dashboard_panel.test", "title", "Errors over time (renamed)"),
|
|
},
|
|
{
|
|
// "dashboard_id/panel_id" -- see ImportState's doc
|
|
// comment on splitImportID for why a bare panel ID
|
|
// isn't enough.
|
|
ResourceName: "sentry_dashboard_panel.test",
|
|
ImportState: true,
|
|
ImportStateIdFunc: func(s *tfstate.State) (string, error) {
|
|
rs, ok := s.RootModule().Resources["sentry_dashboard_panel.test"]
|
|
if !ok {
|
|
return "", fmt.Errorf("sentry_dashboard_panel.test not found in state")
|
|
}
|
|
return rs.Primary.Attributes["dashboard_id"] + "/" + rs.Primary.Attributes["id"], nil
|
|
},
|
|
ImportStateVerify: true,
|
|
},
|
|
},
|
|
})
|
|
}
|