|
| 1 | +# Terraform Provider for Linode - AI Agent Instructions |
| 2 | + |
| 3 | +## Architecture Overview |
| 4 | + |
| 5 | +This is a Terraform provider that uses **both** SDKv2 and Plugin Framework patterns (muxed together). Resources/data sources live under `linode/<resource-name>/` with each package being self-contained. |
| 6 | + |
| 7 | +- **SDKv2 resources** (legacy): `linode/instance/`, `linode/domain/`, `linode/lke/` - use `resource.go`, `datasource.go` |
| 8 | +- **Plugin Framework resources** (preferred for new work): `linode/vpc/`, `linode/volume/`, `linode/vpcsubnet/` - use `framework_resource.go`, `framework_datasource.go`, `framework_models.go` |
| 9 | +- **Provider registration**: SDKv2 in `linode/provider.go`, Framework in `linode/framework_provider.go` |
| 10 | +- **Shared utilities**: `linode/helper/` - conversion functions, base resource/datasource, plan modifiers |
| 11 | + |
| 12 | +## Framework Resource Structure (New Resources) |
| 13 | + |
| 14 | +Each Framework resource package follows this pattern: |
| 15 | + |
| 16 | +``` |
| 17 | +linode/<resource-name>/ |
| 18 | +├── framework_resource.go # CRUD operations |
| 19 | +├── framework_datasource.go # Data source Read |
| 20 | +├── framework_models.go # Terraform state models with Flatten/CopyFrom methods |
| 21 | +├── framework_schema_resource.go # Resource schema definition |
| 22 | +├── framework_schema_datasource.go # Data source schema definition |
| 23 | +├── resource_test.go # Integration tests |
| 24 | +├── datasource_test.go # Data source integration tests |
| 25 | +├── tmpl/ # Test templates |
| 26 | +│ ├── template.go # Go functions returning HCL configs |
| 27 | +│ └── *.gotf # HCL template files |
| 28 | +``` |
| 29 | + |
| 30 | +### Key Model Patterns |
| 31 | + |
| 32 | +Models must implement: |
| 33 | +- `FlattenXxx(ctx, apiObject, preserveKnown)` - Converts Linode API response to Terraform state |
| 34 | +- `CopyFrom(ctx, other, preserveKnown)` - Copies values between model instances for updates |
| 35 | + |
| 36 | +Use `helper.KeepOrUpdateValue()`, `helper.KeepOrUpdateString()`, `helper.KeepOrUpdateInt64()` to handle `preserveKnown` flag which prevents overwriting known plan values with computed values. |
| 37 | + |
| 38 | +## Test Commands |
| 39 | + |
| 40 | +```bash |
| 41 | +# Run all tests for a package |
| 42 | +make TEST_SUITE="vpcsubnet" test-int |
| 43 | + |
| 44 | +# Run a specific test |
| 45 | +make PKG_NAME="volume" TEST_CASE="TestAccResourceVolume_basic" test-int |
| 46 | + |
| 47 | +# Run unit tests only |
| 48 | +make test-unit |
| 49 | + |
| 50 | +# Run unit tests for a specific package |
| 51 | +make PKG_NAME="instance" test-unit |
| 52 | +``` |
| 53 | + |
| 54 | +**Important**: Set `LINODE_TOKEN` environment variable or use `.env` file. Tests create real resources (costs money). |
| 55 | + |
| 56 | +## Test Template Pattern |
| 57 | + |
| 58 | +Tests use `.gotf` template files with Go text/template syntax: |
| 59 | + |
| 60 | +```go |
| 61 | +// In tmpl/template.go |
| 62 | +func Basic(t testing.TB, label, region string) string { |
| 63 | + return acceptance.ExecuteTemplate(t, "resource_basic", TemplateData{ |
| 64 | + Label: label, Region: region, |
| 65 | + }) |
| 66 | +} |
| 67 | +``` |
| 68 | + |
| 69 | +```hcl |
| 70 | +// In tmpl/basic.gotf |
| 71 | +{{ define "resource_basic" }} |
| 72 | +resource "linode_example" "foobar" { |
| 73 | + label = "{{.Label}}" |
| 74 | + region = "{{.Region}}" |
| 75 | +} |
| 76 | +{{ end }} |
| 77 | +``` |
| 78 | + |
| 79 | +## Build Tags |
| 80 | + |
| 81 | +- `//go:build integration` or `//go:build <resource-name>` - Integration tests (require API token) |
| 82 | +- `//go:build unit` - Unit tests (no API calls) |
| 83 | + |
| 84 | +## Helper Functions Reference |
| 85 | + |
| 86 | +| Function | Purpose | |
| 87 | +|----------|---------| |
| 88 | +| `helper.NewBaseResource()` | Creates base Framework resource with common config | |
| 89 | +| `helper.KeepOrUpdateValue()` | Conditionally preserves known values during refresh | |
| 90 | +| `helper.FrameworkSafeInt64ToInt()` | Safe int64→int conversion with diagnostics | |
| 91 | +| `helper.MapSlice()` | Transforms slices with a mapping function | |
| 92 | +| `acceptance.GetRandomRegionWithCaps()` | Gets random region with required capabilities | |
| 93 | +| `acceptance.ExecuteTemplate()` | Renders HCL test templates | |
| 94 | + |
| 95 | +## Common Workflows |
| 96 | + |
| 97 | +**Adding a new Framework resource:** |
| 98 | +1. Create package under `linode/<resource-name>/` |
| 99 | +2. Define schema in `framework_schema_resource.go` |
| 100 | +3. Define models in `framework_models.go` with `Flatten*` and `CopyFrom` methods |
| 101 | +4. Implement CRUD in `framework_resource.go` |
| 102 | +5. Register in `linode/framework_provider.go` Resources() method |
| 103 | +6. Add tests with `tmpl/` directory |
| 104 | +7. Add docs in `docs/resources/<resource>.md` |
| 105 | + |
| 106 | +**Debugging tests:** |
| 107 | +- `TF_LOG_PROVIDER=DEBUG` - Provider logging |
| 108 | +- `TF_LOG_PROVIDER_LINODE_REQUESTS=DEBUG` - API request logging |
| 109 | +- `TF_SCHEMA_PANIC_ON_ERROR=1` - Force panic on schema errors |
| 110 | + |
| 111 | +## Linode API Client |
| 112 | + |
| 113 | +Uses `github.com/linode/linodego` client. Access via: |
| 114 | +- SDKv2: `meta.(*helper.ProviderMeta).Client` |
| 115 | +- Framework: `r.Meta.Client` |
| 116 | + |
| 117 | +## Code Style |
| 118 | + |
| 119 | +- Use `golangci-lint fmt` for code formatting (run `make format`), or `gofmt -w` if unavailable |
| 120 | +- Use `tflog.Debug(ctx, ...)` for logging in resources |
| 121 | +- Prefer Framework over SDKv2 for new resources |
| 122 | +- Unit test files use `*_unit_test.go` naming with `//go:build unit` tag |
| 123 | + |
| 124 | +## Go Idioms |
| 125 | + |
| 126 | +**Sets using maps (Go 1.23+):** |
| 127 | +Use `helper.StringSet` and `helper.ExistsInSet` for set operations, then extract keys with `slices.Collect(maps.Keys())`: |
| 128 | + |
| 129 | +```go |
| 130 | +import ( |
| 131 | + "maps" |
| 132 | + "slices" |
| 133 | + "github.com/linode/terraform-provider-linode/v3/linode/helper" |
| 134 | +) |
| 135 | + |
| 136 | +// Create a set |
| 137 | +regionSet := make(helper.StringSet) |
| 138 | +for _, endpoint := range endpoints { |
| 139 | + regionSet[endpoint.Region] = helper.ExistsInSet |
| 140 | +} |
| 141 | + |
| 142 | +// Extract keys as a slice (Go 1.23+) |
| 143 | +regions := slices.Collect(maps.Keys(regionSet)) |
| 144 | +``` |
| 145 | + |
| 146 | +This is preferred over the manual loop pattern: |
| 147 | +```go |
| 148 | +// Avoid this verbose pattern |
| 149 | +regions := make([]string, 0, len(regionSet)) |
| 150 | +for region := range regionSet { |
| 151 | + regions = append(regions, region) |
| 152 | +} |
| 153 | +``` |
0 commit comments