Scaffold a new AppPlatform Terraform resource with schema, tests, examples, and provider registration. Use when adding a new K8s-backed Grafana resource.
Current AppPlatform resources:
!grep -oE 'appplatform\.[a-zA-Z0-9_]+\(\)' pkg/provider/resources.go
Git status:
!git status --short | head -5
references/checklist.md, references/gotchas.md, references/field-type-mapping.md.After exiting plan mode and before starting Phase 2, create a task list using TaskCreate for every execute step. Mark each task in_progress before starting it and completed when done.
ACTION — Use TaskCreate to create one task per step (adapt based on the approved plan — skip tasks that don't apply):
| # | Subject | activeForm |
|---|---|---|
| 1 | Install SDK dependency | Installing SDK dependency |
| 2 | Create resource implementation | Creating resource implementation |
| 3 | Register resource in provider | Registering in AppPlatformResources |
| 4 | Create example HCL | Creating example HCL |
| 5 | Create acceptance test | Creating acceptance test |
| 6 | Update test gating in examples_test.go | Updating test gating |
| 7 | Build and verify compilation | Running go build |
| 8 | Generate documentation | Running make docs |
Skip #1 if the SDK package is already in go.mod. Skip #6 if no new version gating is needed.
Enter
/planmode before starting Phase 0. Stay in plan mode until the human approves in Phase 1.
If $ARGUMENTS is:
https://github.com/...) → fetch with gh issue view <url>gh issue view <number>Use AskUserQuestion to collect (all required):
| Question | Header | Options / Hint |
|---|---|---|
API group (e.g. alerting.grafana.app) | API Group | free text |
Kind (e.g. AlertEnrichment) | Kind | free text |
Version (e.g. v1beta1) | Version | free text |
Go SDK import path (e.g. github.com/grafana/grafana/apps/alerting/alertenrichment/pkg/apis/alertenrichment/v1beta1) | SDK Import | free text |
| Resource category | Category | CategoryAlerting, CategoryGrafanaApps, CategoryGrafanaEnterprise, CategoryGrafanaOSS |
Spawn an Explore agent to:
go.mod for the SDK import path — note whether go get will be needed.<Kind>Kind() function, <Kind>Spec struct, <Kind>List type.<Kind>Spec struct fields — note field names, Go types, required/optional hints (pointers = optional).If the SDK package is not in go.mod, ask:
"The SDK package
<path>is not in go.mod. Should I rungo get <path>to add it, or would you prefer to hand-roll local K8s types?"
If SDK types don't exist (no <Kind>Spec struct), ask:
"I couldn't find
<Kind>Specin the SDK. Should I hand-roll local K8s types (following theappo11y_config_resource.gopattern), or wait for upstream types?"
Use AskUserQuestion (multi-select OK):
disable_provenance? (See alertenrichment_resource.go for the pattern.)secret_secure_value_resource.go for example.)Use AskUserQuestion:
| Question | Header | Options |
|---|---|---|
| Test category | Test gate | OSS, Enterprise |
| Minimum Grafana version | Min version | free text, e.g. >=12.0.0 |
Read all three reference files:
.claude/skills/add-app-platform-resource/references/checklist.md.claude/skills/add-app-platform-resource/references/gotchas.md.claude/skills/add-app-platform-resource/references/field-type-mapping.mdChoose an existing resource based on complexity:
| Resource | When to use |
|---|---|
internal/resources/appplatform/playlist_resource.go | Simple: flat spec, list of objects |
internal/resources/appplatform/inhibitionrule_resource.go | Medium: list attributes with validators |
internal/resources/appplatform/secret_keeper_resource.go | Complex: nested blocks |
internal/resources/appplatform/secret_secure_value_resource.go | WriteOnly fields + PlanModifier |
internal/resources/appplatform/appo11y_config_resource.go | Hand-rolled K8s types |
Read the chosen file now.
Also read internal/resources/appplatform/alertenrichment_resource_acc_test.go to understand test structure (especially importStateIDFunc).
Using the SDK spec fields from Phase 0.3 and the field-type mapping from the reference file, design:
grafana_apps_{first-segment-of-group}_{lowercase-kind}_{version}
alerting.grafana.app, kind=AlertEnrichment, version=v1beta1 → grafana_apps_alerting_alertenrichment_v1beta1references/field-type-mapping.md)<Name>SpecModel plus nested models as neededImportState)Present a structured summary:
## Implementation Plan
**TF resource name**: grafana_apps_<group>_<kind>_<version>
**Category**: <category>
**Reference resource**: <file used as template>
### Spec Schema
| Field | TF Type | Required/Optional | Description |
|-------|---------|-------------------|-------------|
| ... | ... | ... | ... |
### Advanced features
- Provenance: yes/no
- PlanModifier: yes/no
- UpdateDecider: yes/no
- UseConfigSpec: yes/no
### Test gating
- testutils.Check<OSS|Enterprise>TestsEnabled(t, "<version>")
### Files to create/modify
1. CREATE internal/resources/appplatform/<name>_resource.go
2. CREATE internal/resources/appplatform/<name>_resource_acc_test.go
3. CREATE examples/resources/grafana_apps_<group>_<kind>_<version>/resource.tf
4. MODIFY pkg/provider/resources.go — add appplatform.<FuncName>() to AppPlatformResources()
5. MODIFY internal/resources/examples_test.go — add case for new resource (if new category or special version)
6. GENERATED docs/resources/apps_<group>_<kind>_<version>.md — via make docs
Wait for human approval. If changes requested, iterate on the schema design and re-present.
Exit /plan mode once the human approves.
go get if neededACTION — Call TaskUpdate: set "Install SDK dependency" task to status="in_progress".
If the SDK package was not in go.mod:
go get <import-path>
ACTION — Call TaskUpdate: set "Install SDK dependency" task to status="completed". (Skip if task wasn't created.)
ACTION — Call TaskUpdate: set "Create resource implementation" task to status="in_progress".
Create internal/resources/appplatform/<name>_resource.go.
Structure (follow the reference resource exactly):
package appplatform
import (...)
// <Name>SpecModel is a model for the <name> spec.
type <Name>SpecModel struct {
// fields matching SpecAttributes keys (snake_case), tfsdk tags
}
// <Name>() creates a new Grafana <Name> resource.
func <Name>() NamedResource {
return NewNamedResource[*<pkg>.<Kind>, *<pkg>.<Kind>List](
common.<Category>,
ResourceConfig[*<pkg>.<Kind>]{
Kind: <pkg>.<Kind>Kind(),
Schema: ResourceSpecSchema{
Description: "...",
MarkdownDescription: `...`,
SpecAttributes: map[string]schema.Attribute{
// ... fields
},
// SpecBlocks if needed
},
SpecParser: func(ctx context.Context, src types.Object, dst *<pkg>.<Kind>) diag.Diagnostics {
var data <Name>SpecModel
if diag := src.As(ctx, &data, basetypes.ObjectAsOptions{
UnhandledNullAsEmpty: true,
UnhandledUnknownAsEmpty: true,
}); diag.HasError() {
return diag
}
// map data → dst.SetSpec(...)
return diag.Diagnostics{}
},
SpecSaver: func(ctx context.Context, src *<pkg>.<Kind>, dst *ResourceModel) diag.Diagnostics {
// map src.Spec → types.Object and set dst.Spec
return diag.Diagnostics{}
},
})
}
Key rules from references/gotchas.md:
basetypes.ObjectAsOptions{UnhandledNullAsEmpty: true, UnhandledUnknownAsEmpty: true} in SpecParsertypes.ObjectValueFrom(ctx, map[string]attr.Type{...}, &data) — AttrTypes keys MUST match SpecAttributes keys exactlytypes.StringNull() / types.ListNull(), not zero valuesappo11y_config_resource.go pattern (local Object/ListObject/Kind/Codec types)ACTION — Call TaskUpdate: set "Create resource implementation" task to status="completed".
ACTION — Call TaskUpdate: set "Register resource in provider" task to status="in_progress".
Edit pkg/provider/resources.go, add to AppPlatformResources():
func AppPlatformResources() []appplatform.NamedResource {
return []appplatform.NamedResource{
// ... existing resources ...
appplatform.<Name>(), // ADD THIS LINE
}
}
ACTION — Call TaskUpdate: set "Register resource in provider" task to status="completed".
ACTION — Call TaskUpdate: set "Create example HCL" task to status="in_progress".
Create examples/resources/grafana_apps_<group>_<kind>_<version>/resource.tf:
resource "grafana_apps_<group>_<kind>_<version>" "example" {
metadata {
uid = "my-<kind>"
}
spec {
# ... required fields with realistic values
# optional fields can be omitted or included with comments
}
}
ACTION — Call TaskUpdate: set "Create example HCL" task to status="completed".
ACTION — Call TaskUpdate: set "Create acceptance test" task to status="in_progress".
Create internal/resources/appplatform/<name>_resource_acc_test.go:
package appplatform_test
import (
"fmt"
"testing"
"github.com/grafana/terraform-provider-grafana/v4/internal/testutils"
terraformresource "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
)
const <name>ResourceName = "grafana_apps_<group>_<kind>_<version>.test"
func TestAcc<Name>_basic(t *testing.T) {
testutils.Check<OSS|Enterprise>TestsEnabled(t, "<version>")
randSuffix := acctest.RandString(6)
terraformresource.ParallelTest(t, terraformresource.TestCase{
ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
Steps: []terraformresource.TestStep{
{
Config: testAcc<Name>Basic(randSuffix),
Check: terraformresource.ComposeTestCheckFunc(
terraformresource.TestCheckResourceAttrSet(<name>ResourceName, "id"),
// ... attribute checks
),
},
{
ResourceName: <name>ResourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{
"options.%",
"options.overwrite",
},
ImportStateIdFunc: importStateIDFunc(<name>ResourceName),
},
},
})
}
func testAcc<Name>Basic(randSuffix string) string {
return fmt.Sprintf(`
resource "grafana_apps_<group>_<kind>_<version>" "test" {
metadata {
uid = "test-%s"
}
spec {
# ... required fields
}
}
`, randSuffix)
}
Note: importStateIDFunc is defined in alertenrichment_resource_acc_test.go and is available to all tests in the appplatform_test package.
ACTION — Call TaskUpdate: set "Create acceptance test" task to status="completed".
ACTION — Call TaskUpdate: set "Update test gating in examples_test.go" task to status="in_progress" (skip if no gating change needed).
If the new resource belongs to a new category not already in examples_test.go, or needs special version gating, add a case:
case strings.Contains(filename, "grafana_apps_<group>_<kind>"):
testutils.Check<OSS|Enterprise>TestsEnabled(t, "<version>")
Find the appropriate category block in internal/resources/examples_test.go.
ACTION — Call TaskUpdate: set "Update test gating in examples_test.go" task to status="completed" (if applicable).
ACTION — Call TaskUpdate: set "Build and verify compilation" task to status="in_progress".
go build .
If the build fails:
go build .Common build failures (see references/gotchas.md):
AttrTypes map keys don't match SpecAttributes keys → align thembasetypes, attr, diag → add importstypes.Int64 instead of types.Int64Type) → check field-type-mappingACTION — Call TaskUpdate: set "Build and verify compilation" task to status="completed".
ACTION — Call TaskUpdate: set "Generate documentation" task to status="in_progress".
make docs
Verify docs/resources/apps_<group>_<kind>_<version>.md was created.
ACTION — Call TaskUpdate: set "Generate documentation" task to status="completed".
Ask the human:
"Would you like to run the acceptance test? This requires a live Grafana instance with
GRAFANA_URL,GRAFANA_AUTH, andGRAFANA_VERSIONset, plusTF_ACC_<OSS|ENTERPRISE>=true."
If yes, run:
GRAFANA_URL=http://localhost:3000 GRAFANA_AUTH=admin:admin TF_ACC=1 TF_ACC_<OSS|ENTERPRISE>=true GRAFANA_VERSION=<version> \
go test ./internal/resources/appplatform/... -run TestAcc<Name> -v -timeout 30m
ACTION — Call TaskList and verify all tasks are marked completed. If any were skipped or failed, note them in the summary.
Present a summary:
## Summary
### Files created
- internal/resources/appplatform/<name>_resource.go
- internal/resources/appplatform/<name>_resource_acc_test.go
- examples/resources/grafana_apps_<group>_<kind>_<version>/resource.tf
### Files modified
- pkg/provider/resources.go (added appplatform.<Name>() to AppPlatformResources)
- internal/resources/examples_test.go (if gating was added)
### Generated
- docs/resources/apps_<group>_<kind>_<version>.md (via make docs)
### Next steps
- Review the generated schema and adjust descriptions as needed
- Commit and open a PR
- Consider adding more test cases for edge cases / update scenarios
| File | Purpose |
|---|---|
internal/resources/appplatform/resource.go | Generic framework — ResourceConfig, ResourceModel, NamedResource |
internal/resources/appplatform/playlist_resource.go | Simple resource (flat spec + list of objects) |
internal/resources/appplatform/inhibitionrule_resource.go | Medium complexity (list attributes with validators) |
internal/resources/appplatform/secret_keeper_resource.go | Nested blocks example |
internal/resources/appplatform/secret_secure_value_resource.go | WriteOnly fields + PlanModifier |
internal/resources/appplatform/appo11y_config_resource.go | Hand-rolled K8s types (no SDK) |
internal/resources/appplatform/alertenrichment_resource_acc_test.go | Test patterns + importStateIDFunc |
pkg/provider/resources.go | Registration point (AppPlatformResources at line ~83) |
internal/resources/examples_test.go | Example test gating |
internal/common/resource.go | Category constants |