├── .changes ├── 2.24.1.md ├── 2.25.0.md ├── 2.26.0.md ├── 2.26.1.md ├── 2.27.0.md ├── 2.28.0.md ├── 2.29.0.md ├── 2.30.0.md ├── 2.31.0.md ├── 2.32.0.md ├── 2.33.0.md ├── 2.34.0.md ├── 2.35.0.md ├── 2.36.0.md ├── 2.36.1.md ├── 2.37.0-alpha.1.md ├── 2.37.0-beta.1.md ├── 2.37.0.md └── unreleased │ └── .gitkeep ├── .changie.yaml ├── .copywrite.hcl ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── SUPPORT.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── ci-changie.yml │ ├── ci-github-actions.yml │ ├── ci-go.yml │ ├── ci-goreleaser.yml │ ├── compliance.yml │ ├── issue-comment-triage.yml │ ├── lock.yml │ └── release.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── CHANGELOG.md ├── LICENSE ├── META.d └── _summary.yaml ├── Makefile ├── README.md ├── ROADMAP.md ├── SUPPORT.md ├── diag ├── diagnostic.go └── helpers.go ├── go.mod ├── go.sum ├── helper ├── acctest │ ├── random.go │ └── random_test.go ├── customdiff │ ├── compose.go │ ├── compose_test.go │ ├── computed.go │ ├── computed_test.go │ ├── condition.go │ ├── condition_test.go │ ├── doc.go │ ├── force_new.go │ ├── force_new_test.go │ ├── testing_test.go │ ├── validate.go │ └── validate_test.go ├── id │ ├── id.go │ └── id_test.go ├── logging │ ├── logging.go │ ├── logging_http_transport.go │ ├── logging_http_transport_test.go │ └── transport.go ├── resource │ ├── aliases.go │ ├── environment_variables.go │ ├── json.go │ ├── plugin.go │ ├── plugin_test.go │ ├── state_shim.go │ ├── testcase_providers.go │ ├── testcase_providers_test.go │ ├── testcase_validate.go │ ├── testcase_validate_test.go │ ├── testing.go │ ├── testing_config.go │ ├── testing_example_test.go │ ├── testing_new.go │ ├── testing_new_config.go │ ├── testing_new_config_test.go │ ├── testing_new_import_state.go │ ├── testing_new_import_state_test.go │ ├── testing_new_refresh_state.go │ ├── testing_new_test.go │ ├── testing_sets.go │ ├── testing_sets_example_test.go │ ├── testing_sets_test.go │ ├── testing_test.go │ ├── teststep_providers.go │ ├── teststep_providers_test.go │ ├── teststep_validate.go │ └── teststep_validate_test.go ├── retry │ ├── error.go │ ├── state.go │ ├── state_test.go │ ├── wait.go │ └── wait_test.go ├── schema │ ├── README.md │ ├── context.go │ ├── core_schema.go │ ├── core_schema_test.go │ ├── data_source_resource_shim.go │ ├── deferred.go │ ├── equal.go │ ├── field_reader.go │ ├── field_reader_config.go │ ├── field_reader_config_test.go │ ├── field_reader_diff.go │ ├── field_reader_diff_test.go │ ├── field_reader_map.go │ ├── field_reader_map_test.go │ ├── field_reader_multi.go │ ├── field_reader_multi_test.go │ ├── field_reader_test.go │ ├── field_writer.go │ ├── field_writer_map.go │ ├── field_writer_map_test.go │ ├── getsource_string.go │ ├── grpc_provider.go │ ├── grpc_provider_test.go │ ├── identity_data.go │ ├── identity_data_test.go │ ├── json.go │ ├── provider.go │ ├── provider_test.go │ ├── resource.go │ ├── resource_data.go │ ├── resource_data_get_source.go │ ├── resource_data_test.go │ ├── resource_diff.go │ ├── resource_diff_test.go │ ├── resource_identity.go │ ├── resource_identity_test.go │ ├── resource_importer.go │ ├── resource_importer_test.go │ ├── resource_test.go │ ├── resource_timeout.go │ ├── resource_timeout_test.go │ ├── schema.go │ ├── schema_test.go │ ├── serialize.go │ ├── serialize_test.go │ ├── set.go │ ├── set_test.go │ ├── shims.go │ ├── shims_test.go │ ├── testing.go │ ├── unknown.go │ ├── unknown_test.go │ ├── valuetype.go │ ├── valuetype_string.go │ ├── write_only.go │ └── write_only_test.go ├── structure │ ├── expand_json.go │ ├── expand_json_test.go │ ├── flatten_json.go │ ├── flatten_json_test.go │ ├── normalize_json.go │ ├── normalize_json_test.go │ ├── suppress_json_diff.go │ └── suppress_json_diff_test.go └── validation │ ├── float.go │ ├── float_test.go │ ├── int.go │ ├── int_test.go │ ├── list.go │ ├── list_test.go │ ├── map.go │ ├── map_test.go │ ├── meta.go │ ├── meta_test.go │ ├── network.go │ ├── network_test.go │ ├── path.go │ ├── path_test.go │ ├── strings.go │ ├── strings_test.go │ ├── testing.go │ ├── time.go │ ├── time_test.go │ ├── uuid.go │ ├── uuid_test.go │ ├── web.go │ ├── web_test.go │ ├── write_only.go │ └── write_only_test.go ├── internal ├── addrs │ ├── doc.go │ ├── instance_key.go │ ├── module.go │ └── module_instance.go ├── configs │ ├── configschema │ │ ├── coerce_value.go │ │ ├── coerce_value_test.go │ │ ├── doc.go │ │ ├── empty_value.go │ │ ├── empty_value_test.go │ │ ├── implied_type.go │ │ ├── implied_type_test.go │ │ ├── nestingmode_string.go │ │ └── schema.go │ └── hcl2shim │ │ ├── flatmap.go │ │ ├── flatmap_test.go │ │ ├── paths.go │ │ ├── paths_test.go │ │ ├── values.go │ │ ├── values_equiv.go │ │ ├── values_equiv_test.go │ │ └── values_test.go ├── diagutils │ └── diagutils.go ├── helper │ └── hashcode │ │ ├── hashcode.go │ │ └── hashcode_test.go ├── logging │ ├── context.go │ ├── context_test.go │ ├── environment_variables.go │ ├── helper_resource.go │ ├── helper_resource_test.go │ ├── helper_schema.go │ ├── helper_schema_test.go │ └── keys.go ├── plans │ └── objchange │ │ ├── normalize_obj.go │ │ └── normalize_obj_test.go ├── plugin │ └── convert │ │ ├── diagnostics.go │ │ ├── diagnostics_test.go │ │ ├── schema.go │ │ └── schema_test.go ├── plugintest │ ├── config.go │ ├── doc.go │ ├── environment_variables.go │ ├── guard.go │ ├── helper.go │ ├── util.go │ ├── working_dir.go │ └── working_dir_json_test.go ├── providers │ └── provider.go └── tfdiags │ ├── config_traversals.go │ ├── contextual.go │ ├── contextual_test.go │ ├── diagnostic.go │ ├── diagnostic_base.go │ ├── diagnostics.go │ ├── diagnostics_test.go │ ├── doc.go │ ├── error.go │ ├── severity_string.go │ └── simple_warning.go ├── meta └── meta.go ├── plugin ├── debug.go └── serve.go ├── terraform ├── diff.go ├── diff_test.go ├── instancetype.go ├── instancetype_string.go ├── resource.go ├── resource_address.go ├── resource_address_test.go ├── resource_mode.go ├── resource_mode_string.go ├── resource_provider.go ├── resource_test.go ├── schemas.go ├── state.go ├── state_filter.go ├── state_test.go ├── util.go └── util_test.go ├── tools ├── copywrite.go ├── go.mod └── go.sum └── website ├── Makefile ├── README.md ├── data └── plugin-sdkv2-nav-data.json ├── docs └── plugin │ └── sdkv2 │ ├── best-practices │ ├── deprecations.mdx │ ├── detecting-drift.mdx │ └── index.mdx │ ├── debugging.mdx │ ├── guides │ ├── terraform-0.12-compatibility.mdx │ ├── v1-upgrade-guide.mdx │ └── v2-upgrade-guide.mdx │ ├── index.mdx │ ├── logging │ ├── http-transport.mdx │ └── index.mdx │ ├── resources │ ├── customizing-differences.mdx │ ├── data-consistency-errors.mdx │ ├── import.mdx │ ├── index.mdx │ ├── retries-and-customizable-timeouts.mdx │ ├── state-migration.mdx │ └── write-only-arguments.mdx │ ├── schemas │ ├── index.mdx │ ├── schema-behaviors.mdx │ ├── schema-methods.mdx │ └── schema-types.mdx │ └── testing │ ├── acceptance-tests │ ├── index.mdx │ ├── sweepers.mdx │ ├── testcase.mdx │ └── teststep.mdx │ ├── index.mdx │ ├── testing-api.mdx │ ├── testing-patterns.mdx │ └── unit-testing.mdx ├── img └── .gitkeep ├── package-lock.json ├── package.json └── scripts ├── should-build.sh ├── website-build.sh └── website-start.sh /.changes/2.25.0.md: -------------------------------------------------------------------------------- 1 | ## 2.25.0 (February 15, 2023) 2 | 3 | BUG FIXES: 4 | 5 | * helper/schema: Allow diagnostic messages with incorrect UTF-8 encoding to pass through with the invalid sequences replaced with the Unicode Replacement Character. This avoids returning the unhelpful message "string field contains invalid UTF-8" in that case. ([#1111](https://github.com/hashicorp/terraform-plugin-sdk/issues/1111)) 6 | * helper/schema: Prevented unexpected difference for timeouts on first plan after import ([#1146](https://github.com/hashicorp/terraform-plugin-sdk/issues/1146)) 7 | 8 | -------------------------------------------------------------------------------- /.changes/2.26.0.md: -------------------------------------------------------------------------------- 1 | ## 2.26.0 (March 20, 2023) 2 | 3 | NOTES: 4 | 5 | * This Go module has been updated to Go 1.19 per the [Go support policy](https://golang.org/doc/devel/release.html#policy). Any consumers building on earlier Go versions may experience errors. ([#1163](https://github.com/hashicorp/terraform-plugin-sdk/issues/1163)) 6 | * helper/resource: Deprecated `PrefixedUniqueId()` and `UniqueId()`. Use the `helper/id` package instead. These deprecations are to assist in migrating to terraform-plugin-testing ([#1167](https://github.com/hashicorp/terraform-plugin-sdk/issues/1167)) 7 | * helper/resource: Deprecated `RetryContext()`, `StateChangeConf`, and associated `*Error` types. Use the `helper/retry` package instead. These deprecations are to assist in migrating to terraform-plugin-testing ([#1167](https://github.com/hashicorp/terraform-plugin-sdk/issues/1167)) 8 | 9 | ENHANCEMENTS: 10 | 11 | * helper/id: New `helper/id` package added. `resource.PrefixedUniqueId()` and `resource.UniqueId()` are deprecated, `helper/id` should be used instead. `helper/resource` now contains aliases to the migrated code ([#1167](https://github.com/hashicorp/terraform-plugin-sdk/issues/1167)) 12 | * helper/retry: New `helper/retry` package added. `resource.RetryContext()`, `resource.StateChangeConf`, and associated `*Error` types are deprecated, `helper/retry` should be used instead. `helper/resource now contains aliases to the migrated code ([#1167](https://github.com/hashicorp/terraform-plugin-sdk/issues/1167)) 13 | 14 | -------------------------------------------------------------------------------- /.changes/2.26.1.md: -------------------------------------------------------------------------------- 1 | ## 2.26.1 (March 21, 2023) 2 | 3 | BUG FIXES: 4 | 5 | * helper/resource: Prevented build errors with type aliasing added in v2.26.0 ([#1176](https://github.com/hashicorp/terraform-plugin-sdk/issues/1176)) 6 | 7 | -------------------------------------------------------------------------------- /.changes/2.27.0.md: -------------------------------------------------------------------------------- 1 | ## 2.27.0 (June 28, 2023) 2 | 3 | NOTES: 4 | 5 | * helper/schema: Consumers directly referencing the `Resource` type `Schema` field should switch to the `SchemaMap` method to ensure new `SchemaFunc` field data is properly retrieved ([#1217](https://github.com/hashicorp/terraform-plugin-sdk/issues/1217)) 6 | 7 | ENHANCEMENTS: 8 | 9 | * all: Improved SDK logging performance when messages would be skipped due to configured logging level ([#1202](https://github.com/hashicorp/terraform-plugin-sdk/issues/1202)) 10 | * helper/schema: Added `Resource` type `SchemaFunc` field and `SchemaMap` method, which can reduce resident memory usage with large schemas ([#1217](https://github.com/hashicorp/terraform-plugin-sdk/issues/1217)) 11 | 12 | -------------------------------------------------------------------------------- /.changes/2.28.0.md: -------------------------------------------------------------------------------- 1 | ## 2.28.0 (August 24, 2023) 2 | 3 | NOTES: 4 | 5 | * helper/schema: The `Resource` type `EnableApplyLegacyTypeSystemErrors` and `EnablePlanLegacyTypeSystemErrors` fields can be enabled to more easily discover resource data consistency errors which Terraform would normally demote to warning logs. Before enabling the flag in a production release for a resource, the resource should be exhaustively acceptance tested as there may be unrecoverable error situations for practitioners. It is recommended to first enable and test in environments where it is easy to clean up resources, potentially outside of Terraform. ([#1227](https://github.com/hashicorp/terraform-plugin-sdk/issues/1227)) 6 | 7 | ENHANCEMENTS: 8 | 9 | * helper/schema: Added `Resource` type `EnableLegacyTypeSystemApplyErrors` field, which will prevent Terraform from demoting data consistency errors to warning logs during `ApplyResourceChange` (`Create`, `Update`, and `Delete`) operations with the resource ([#1227](https://github.com/hashicorp/terraform-plugin-sdk/issues/1227)) 10 | * helper/schema: Added `Resource` type `EnableLegacyTypeSystemPlanErrors` field, which can be used to prevent Terraform from demoting data consistency errors to warning logs during `PlanResourceChange` operations with the resource ([#1227](https://github.com/hashicorp/terraform-plugin-sdk/issues/1227)) 11 | 12 | -------------------------------------------------------------------------------- /.changes/2.29.0.md: -------------------------------------------------------------------------------- 1 | ## 2.29.0 (September 06, 2023) 2 | 3 | NOTES: 4 | 5 | * all: This Go module has been updated to Go 1.20 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.20 release notes](https://go.dev/doc/go1.20) before upgrading. Any consumers building on earlier Go versions may experience errors. ([#1245](https://github.com/hashicorp/terraform-plugin-sdk/issues/1245)) 6 | 7 | FEATURES: 8 | 9 | * helper/schema: Upgrade to protocol version 5.4, which can significantly reduce memory usage with Terraform 1.6 and later when a configuration includes multiple instances of the same provider ([#1234](https://github.com/hashicorp/terraform-plugin-sdk/issues/1234)) 10 | 11 | ENHANCEMENTS: 12 | 13 | * helper/validation: Added `AllDiag` and `AnyDiag`, which are `SchemaValidateDiagFunc` variants of `All` and `Any` ([#1155](https://github.com/hashicorp/terraform-plugin-sdk/issues/1155)) 14 | * helper/validation: Added quoting in `StringInSlice` error diagnostic output to prevent confusion with values that contain spaces ([#464](https://github.com/hashicorp/terraform-plugin-sdk/issues/464)) 15 | 16 | -------------------------------------------------------------------------------- /.changes/2.30.0.md: -------------------------------------------------------------------------------- 1 | ## 2.30.0 (November 09, 2023) 2 | 3 | NOTES: 4 | 5 | * meta: The `SDKVersion` variable, `SDKPrerelease` variable, and `SDKVersionString()` function have been deprecated. Use the Go standard library `runtime/debug` package build information instead. ([#1257](https://github.com/hashicorp/terraform-plugin-sdk/issues/1257)) 6 | 7 | BUG FIXES: 8 | 9 | * meta: Fixed version in `SDKVersion` variable and `SDKVersionString()` function ([#1257](https://github.com/hashicorp/terraform-plugin-sdk/issues/1257)) 10 | * helper/schema: Ensured `(ResourceData).GetRawConfig()` data is populated for `Provider.ConfigureFunc` and `Provider.ConfigureContextFunc` ([#1270](https://github.com/hashicorp/terraform-plugin-sdk/issues/1270)) 11 | * helper/schema: Ensured `(ResourceData).GetOkExists()` second result is `true` when configuration contains zero-value data in `Provider.ConfigureFunc` and `Provider.ConfigureContextFunc` ([#1270](https://github.com/hashicorp/terraform-plugin-sdk/issues/1270)) 12 | 13 | -------------------------------------------------------------------------------- /.changes/2.31.0.md: -------------------------------------------------------------------------------- 1 | ## 2.31.0 (December 14, 2023) 2 | 3 | NOTES: 4 | 5 | * helper/schema: While this Go module will not receive support for provider-defined functions, the provider server is updated to handle the new operations, which will be required to prevent errors when updating terraform-plugin-framework or terraform-plugin-mux in the future. ([#1288](https://github.com/hashicorp/terraform-plugin-sdk/issues/1288)) 6 | 7 | -------------------------------------------------------------------------------- /.changes/2.32.0.md: -------------------------------------------------------------------------------- 1 | ## 2.32.0 (January 29, 2024) 2 | 3 | NOTES: 4 | 5 | * helper/schema: While this Go module will not receive support for moving resource state across resource types, the provider server is updated to handle the new operation, which will be required to prevent errors when updating terraform-plugin-framework or terraform-plugin-mux in the future. ([#1307](https://github.com/hashicorp/terraform-plugin-sdk/issues/1307)) 6 | 7 | -------------------------------------------------------------------------------- /.changes/2.33.0.md: -------------------------------------------------------------------------------- 1 | ## 2.33.0 (February 23, 2024) 2 | 3 | NOTES: 4 | 5 | * helper/schema: While this Go module will not receive support for provider defined functions, the provider server is updated to handle the new operation, which will be required to prevent errors when updating terraform-plugin-framework or terraform-plugin-mux in the future ([#1316](https://github.com/hashicorp/terraform-plugin-sdk/issues/1316)) 6 | 7 | -------------------------------------------------------------------------------- /.changes/2.34.0.md: -------------------------------------------------------------------------------- 1 | ## 2.34.0 (May 17, 2024) 2 | 3 | NOTES: 4 | 5 | * all: The `v2.33.0` release updated this Go module to Go 1.21 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.21 release notes](https://go.dev/doc/go1.21) before upgrading. Any consumers building on earlier Go versions may experience errors ([#1318](https://github.com/hashicorp/terraform-plugin-sdk/issues/1318)) 6 | * This release contains support for deferred actions, which is an experimental feature only available in prerelease builds of Terraform 1.9 and later. This functionality is subject to change and is not protected by version compatibility guarantees. ([#1335](https://github.com/hashicorp/terraform-plugin-sdk/issues/1335)) 7 | 8 | FEATURES: 9 | 10 | * helper/schema: Added `(Provider).ConfigureProvider` function for configuring providers that support additional features, such as deferred actions. ([#1335](https://github.com/hashicorp/terraform-plugin-sdk/issues/1335)) 11 | * helper/schema: Added `(Resource).ResourceBehavior` to allow additional control over deferred action behavior during plan modification. ([#1335](https://github.com/hashicorp/terraform-plugin-sdk/issues/1335)) 12 | 13 | -------------------------------------------------------------------------------- /.changes/2.35.0.md: -------------------------------------------------------------------------------- 1 | ## 2.35.0 (October 31, 2024) 2 | 3 | NOTES: 4 | 5 | * all: This Go module has been updated to Go 1.22 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.22 release notes](https://go.dev/doc/go1.22) before upgrading. Any consumers building on earlier Go versions may experience errors. ([#1373](https://github.com/hashicorp/terraform-plugin-sdk/issues/1373)) 6 | * helper/schema: While this Go module will not receive support for ephemeral resource types, the provider server is updated to handle the new operations, which will be required to prevent errors when updating `terraform-plugin-framework` or `terraform-plugin-mux` in the future. ([#1390](https://github.com/hashicorp/terraform-plugin-sdk/issues/1390)) 7 | 8 | -------------------------------------------------------------------------------- /.changes/2.36.0.md: -------------------------------------------------------------------------------- 1 | ## 2.36.0 (February 04, 2025) 2 | 3 | NOTES: 4 | 5 | * Write-only attribute support is in technical preview and offered without compatibility promises until Terraform 1.11 is generally available. ([#1375](https://github.com/hashicorp/terraform-plugin-sdk/issues/1375)) 6 | 7 | FEATURES: 8 | 9 | * helper/schema: Added `WriteOnly` schema behavior for managed resource schemas to indicate a write-only attribute. Write-only attribute values are not saved to the Terraform plan or state artifacts. ([#1375](https://github.com/hashicorp/terraform-plugin-sdk/issues/1375)) 10 | * helper/validation: Added `PreferWriteOnlyAttribute()` validator that warns practitioners when a write-only version of a configured attribute is available. ([#1375](https://github.com/hashicorp/terraform-plugin-sdk/issues/1375)) 11 | * schema/resource: Added `ValidateRawResourceConfigFuncs` field which allows resources to define validation logic during the `ValidateResourceTypeConfig` RPC. ([#1375](https://github.com/hashicorp/terraform-plugin-sdk/issues/1375)) 12 | 13 | -------------------------------------------------------------------------------- /.changes/2.36.1.md: -------------------------------------------------------------------------------- 1 | ## 2.36.1 (February 19, 2025) 2 | 3 | NOTES: 4 | 5 | * Write-only attribute support is in technical preview and offered without compatibility promises until Terraform 1.11 is generally available. ([#1375](https://github.com/hashicorp/terraform-plugin-sdk/issues/1375)) 6 | 7 | BUG FIXES: 8 | 9 | * helper/schema: Fixed bug that allowed write-only attributes within set nested blocks. Any attribute within a set nested block with `WriteOnly` set to `true` will now trigger an error message. ([#1427](https://github.com/hashicorp/terraform-plugin-sdk/issues/1427)) 10 | 11 | -------------------------------------------------------------------------------- /.changes/2.37.0-alpha.1.md: -------------------------------------------------------------------------------- 1 | ## 2.37.0-alpha.1 (March 20, 2025) 2 | 3 | NOTES: 4 | 5 | * all: This Go module has been updated to Go 1.23 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.23 release notes](https://go.dev/doc/go1.23) before upgrading. Any consumers building on earlier Go versions may experience errors. ([#1445](https://github.com/hashicorp/terraform-plugin-sdk/issues/1445)) 6 | * This alpha pre-release contains an initial implementation for managed resource identity, which can used with Terraform v1.12.0-alpha20250319, to store and read identity data during plan and apply workflows. A managed resource identity can be used by defining an identity schema in the `resource.Identity` field. Once the identity schema is defined, you can read and store identity data in the new IdentityData struct that is available via the new `Identity()` method on ResourceData and ResourceDiff structs. ([#1444](https://github.com/hashicorp/terraform-plugin-sdk/issues/1444)) 7 | 8 | -------------------------------------------------------------------------------- /.changes/2.37.0-beta.1.md: -------------------------------------------------------------------------------- 1 | ## 2.37.0-beta.1 (April 18, 2025) 2 | 3 | NOTES: 4 | 5 | * This beta pre-release continues the implementation of managed resource identity, which should now be used with Terraform v1.12.0-beta2. Managed resources now can support import by identity during plan and apply workflows. Managed resources that already support import via the `schema.Resource.Importer` field still need to set an ID during import when an identity is provided. The `RequiredForImport` and `OptionalForImport` fields on the identity schema can be used to control the validation that Terraform core will apply to the import config block. ([#1463](https://github.com/hashicorp/terraform-plugin-sdk/issues/1463)) 6 | 7 | -------------------------------------------------------------------------------- /.changes/2.37.0.md: -------------------------------------------------------------------------------- 1 | ## 2.37.0 (May 16, 2025) 2 | 3 | NOTES: 4 | 5 | * all: This Go module has been updated to Go 1.23 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.23 release notes](https://go.dev/doc/go1.23) before upgrading. Any consumers building on earlier Go versions may experience errors. ([#1445](https://github.com/hashicorp/terraform-plugin-sdk/issues/1445)) 6 | * all: This release contains new fields and structs for implmenting managed resource identity. Resource identity is data that is defined by a separate schema and is stored alongside resource state. Identity data is used by Terrform to uniquely identify a remote object and is meant to be immutable during the remote object's lifecycle. Resources that support identity can now be imported using the `identity` attribute in Terraform configuration `import` blocks, available in Terraform v1.12+. The `resource.Identity` field on the `schema.Resource` struct can be used to support identity by defining an identity schema. Once the identity schema is defined, you can read and store identity data in the state file with the new `IdentityData` struct that is available via the `Identity()` method on `schema.ResourceData` and `schema.ResourceDiff` structs. ([#1444](https://github.com/hashicorp/terraform-plugin-sdk/issues/1444)) 7 | 8 | FEATURES: 9 | 10 | * helper/schema: Added new `TestResourceDataWithIdentityRaw` function for creating a `ResourceData` struct with identity data for unit testing. ([#1475](https://github.com/hashicorp/terraform-plugin-sdk/issues/1475)) 11 | * helper/schema: Added new `Identity` field to `Resource` that supports defining an identity schema for managed resources only. ([#1444](https://github.com/hashicorp/terraform-plugin-sdk/issues/1444)) 12 | * Added new `ImportStatePassthroughWithIdentity` helper that can support both identity and ID importing via a single field. ([#1474](https://github.com/hashicorp/terraform-plugin-sdk/issues/1474)) 13 | 14 | ENHANCEMENTS: 15 | 16 | * helper/schema: Added `RequiredForImport` and `OptionalForImport` fields to the `Schema` struct, which are only valid for identity schemas. ([#1444](https://github.com/hashicorp/terraform-plugin-sdk/issues/1444)) 17 | * helper/schema: Updated `ResourceData` to support passing of identity data in CRUD and import functions for managed resources. ([#1444](https://github.com/hashicorp/terraform-plugin-sdk/issues/1444)) 18 | 19 | BUG FIXES: 20 | 21 | * helper/schema: Fixed bug that blocked write-only attributes from being used with resources without update functions. ([#1472](https://github.com/hashicorp/terraform-plugin-sdk/issues/1472)) 22 | 23 | -------------------------------------------------------------------------------- /.changes/unreleased/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp/terraform-plugin-sdk/289cf91e0721406e2c9c0122cc7685cab7a7df27/.changes/unreleased/.gitkeep -------------------------------------------------------------------------------- /.changie.yaml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT - This GitHub Workflow is managed by automation 2 | # https://github.com/hashicorp/terraform-devex-repos 3 | changesDir: .changes 4 | unreleasedDir: unreleased 5 | changelogPath: CHANGELOG.md 6 | versionExt: md 7 | versionFormat: '## {{.Version}} ({{.Time.Format "January 02, 2006"}})' 8 | kindFormat: '{{.Kind}}:' 9 | changeFormat: '* {{.Body}} ([#{{.Custom.Issue}}](https://github.com/hashicorp/terraform-plugin-sdk/issues/{{.Custom.Issue}}))' 10 | custom: 11 | - key: Issue 12 | label: Issue/PR Number 13 | type: int 14 | minInt: 1 15 | kinds: 16 | - label: BREAKING CHANGES 17 | - label: NOTES 18 | - label: FEATURES 19 | - label: ENHANCEMENTS 20 | - label: BUG FIXES 21 | newlines: 22 | afterKind: 1 23 | beforeKind: 1 24 | endOfVersion: 2 25 | -------------------------------------------------------------------------------- /.copywrite.hcl: -------------------------------------------------------------------------------- 1 | schema_version = 1 2 | 3 | project { 4 | license = "MPL-2.0" 5 | copyright_year = 2019 6 | 7 | header_ignore = [ 8 | # internal catalog metadata (prose) 9 | "META.d/**/*.yaml", 10 | 11 | # changie tooling configuration and CHANGELOG entries (prose) 12 | ".changes/unreleased/**", 13 | ".changie.yaml", 14 | 15 | # GitHub issue template configuration 16 | ".github/ISSUE_TEMPLATE/*.yml", 17 | 18 | # GitHub Actions workflow-specific configurations 19 | ".github/labeler-*.yml", 20 | 21 | # golangci-lint tooling configuration 22 | ".golangci.yml", 23 | 24 | # GoReleaser tooling configuration 25 | ".goreleaser.yml", 26 | 27 | # Release Engineering tooling configuration 28 | ".release/*.hcl", 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hashicorp/terraform-devex 2 | 3 | # engineering and web presence get notified of, and can approve changes to web tooling, but not content. 4 | 5 | /website/ @hashicorp/web-presence @hashicorp/terraform-devex 6 | /website/data/ 7 | /website/public/ 8 | /website/content/ 9 | 10 | # education and engineering get notified of, and can approve changes to web content. 11 | 12 | /website/data/ @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-devex 13 | /website/public/ @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-devex 14 | /website/content/ @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-devex 15 | /website/docs/ @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-devex 16 | /website/img/ @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-devex 17 | /website/README.md @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-devex -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | HashiCorp Community Guidelines apply to you when interacting with the community here on GitHub and contributing code. 4 | 5 | Please read the full text at https://www.hashicorp.com/community-guidelines 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug report 3 | about: Let us know about an unexpected error, a crash, or an incorrect behavior. 4 | labels: bug 5 | --- 6 | 7 | ### SDK version 8 | 20 | 21 | ``` 22 | ... 23 | ``` 24 | 25 | ### Relevant provider source code 26 | 27 | 31 | ```go 32 | ... 33 | ``` 34 | 35 | ### Terraform Configuration Files 36 | 41 | 42 | ```hcl 43 | ... 44 | ``` 45 | 46 | ### Debug Output 47 | 52 | 53 | 54 | ### Expected Behavior 55 | 58 | 59 | ### Actual Behavior 60 | 63 | 64 | ### Steps to Reproduce 65 | 70 | 71 | ### References 72 | 77 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Provider-related Feedback and Questions 4 | url: https://github.com/terraform-providers 5 | about: Each provider (e.g. AWS, Azure, GCP, Oracle, K8S, etc.) has its own repository, any provider related issues or questions should be directed to appropriate provider repository. 6 | - name: Terraform Language or Workflow Feedback and Questions 7 | url: https://github.com/hashicorp/terraform/issues/new/choose 8 | about: Terraform Core has its own repository, any language (HCL) or workflow related issues or questions should be directed there. 9 | - name: ❓ Plugin SDK Questions 10 | url: https://discuss.hashicorp.com/c/terraform-providers/tf-plugin-sdk 11 | about: Please ask and answer SDK related questions through the Plugin SDK Community Forum. 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature or other enhancement. 4 | labels: enhancement 5 | --- 6 | 7 | ### SDK version 8 | 20 | ``` 21 | ... 22 | ``` 23 | 24 | ### Use-cases 25 | 30 | 31 | ### Attempted Solutions 32 | 39 | 40 | ### Proposal 41 | 48 | 49 | ### References 50 | 55 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | Terraform is a mature project with a growing community. There are active, dedicated people willing to help you through various mediums. 4 | 5 | Take a look at those mediums listed at https://www.terraform.io/community.html 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | open-pull-requests-limit: 20 8 | ignore: 9 | # go-hclog should only be updated via terraform-plugin-log 10 | - dependency-name: "github.com/hashicorp/go-hclog" 11 | # go-plugin should only be updated via terraform-plugin-go 12 | - dependency-name: "github.com/hashicorp/go-plugin" 13 | - package-ecosystem: "gomod" 14 | directory: "/tools" 15 | schedule: 16 | interval: "daily" 17 | - package-ecosystem: "github-actions" 18 | directory: "/" 19 | schedule: 20 | interval: "daily" 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Related Issue 2 | 3 | Fixes # 4 | 5 | ## Description 6 | 7 | In plain English, describe your approach to addressing the issue linked above. For example, if you made a particular design decision, let us know why you chose this path instead of another solution. 8 | 9 | 10 | ## Rollback Plan 11 | 12 | - [ ] If a change needs to be reverted, we will roll out an update to the code within 7 days. 13 | 14 | ## Changes to Security Controls 15 | 16 | Are there any changes to security controls (access controls, encryption, logging) in this pull request? If so, explain. 17 | -------------------------------------------------------------------------------- /.github/workflows/ci-changie.yml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT - This GitHub Workflow is managed by automation 2 | # https://github.com/hashicorp/terraform-devex-repos 3 | 4 | # Continuous integration handling for changie 5 | name: ci-changie 6 | 7 | on: 8 | pull_request: 9 | paths: 10 | - .changes/unreleased/*.yaml 11 | - .changie.yaml 12 | - .github/workflows/ci-changie.yml 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | check: 19 | runs-on: ubuntu-latest 20 | steps: 21 | # Ensure terraform-devex-repos is updated on version changes. 22 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 23 | # Ensure terraform-devex-repos is updated on version changes. 24 | - uses: miniscruff/changie-action@6dcc2533cac0495148ed4046c438487e4dceaa23 # v2.0.0 25 | with: 26 | version: latest 27 | args: batch patch --dry-run 28 | -------------------------------------------------------------------------------- /.github/workflows/ci-github-actions.yml: -------------------------------------------------------------------------------- 1 | # Continuous integration handling for GitHub Actions workflows 2 | name: ci-github-actions 3 | 4 | on: 5 | pull_request: 6 | paths: 7 | - .github/workflows/*.yml 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | actionlint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 18 | with: 19 | go-version-file: 'go.mod' 20 | - run: go install github.com/rhysd/actionlint/cmd/actionlint@latest 21 | - run: actionlint 22 | -------------------------------------------------------------------------------- /.github/workflows/ci-go.yml: -------------------------------------------------------------------------------- 1 | # Continuous integration handling for Go 2 | name: ci-go 3 | 4 | on: 5 | pull_request: 6 | paths: 7 | - .github/workflows/ci-go.yml 8 | - .golangci.yml 9 | - go.mod 10 | - '**.go' 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | golangci-lint: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 20 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 21 | with: 22 | go-version-file: 'go.mod' 23 | - run: go mod download 24 | - uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 25 | with: 26 | version: latest 27 | terraform-provider-corner-tfprotov5: 28 | defaults: 29 | run: 30 | working-directory: terraform-provider-corner 31 | name: terraform-provider-corner (tfprotov5 / Terraform ${{ matrix.terraform}}) 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 35 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 36 | with: 37 | path: terraform-provider-corner 38 | repository: hashicorp/terraform-provider-corner 39 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 40 | with: 41 | go-version-file: 'go.mod' 42 | - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 43 | with: 44 | terraform_version: ${{ matrix.terraform }} 45 | terraform_wrapper: false 46 | - run: go mod edit -replace=github.com/hashicorp/terraform-plugin-sdk/v2=../ 47 | - run: go mod tidy 48 | - run: go test -v ./internal/sdkv2provider 49 | env: 50 | TF_ACC: '1' 51 | strategy: 52 | fail-fast: false 53 | matrix: 54 | terraform: ${{ fromJSON(vars.TF_VERSIONS_PROTOCOL_V5) }} 55 | test: 56 | name: test (Go v${{ matrix.go-version }}) 57 | runs-on: ubuntu-latest 58 | strategy: 59 | matrix: 60 | go-version: [ '1.24', '1.23' ] 61 | steps: 62 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 63 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 64 | with: 65 | go-version: ${{ matrix.go-version }} 66 | - run: go mod download 67 | - run: go test -coverprofile=coverage.out ./... 68 | - run: go tool cover -html=coverage.out -o coverage.html 69 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 70 | with: 71 | name: go-${{ matrix.go-version }}-coverage 72 | path: coverage.html 73 | -------------------------------------------------------------------------------- /.github/workflows/ci-goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Continuous integration handling for GoReleaser 2 | name: ci-goreleaser 3 | 4 | on: 5 | pull_request: 6 | paths: 7 | - .github/workflows/ci-goreleaser.yml 8 | - .goreleaser.yml 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | check: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 18 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 19 | with: 20 | go-version-file: 'go.mod' 21 | - uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 22 | with: 23 | args: check 24 | -------------------------------------------------------------------------------- /.github/workflows/compliance.yml: -------------------------------------------------------------------------------- 1 | name: compliance 2 | 3 | on: 4 | pull_request: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | # Reference: ENGSRV-059 11 | copywrite: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 15 | - uses: hashicorp/setup-copywrite@32638da2d4e81d56a0764aa1547882fc4d209636 # v1.1.3 16 | - run: copywrite headers --plan 17 | - run: copywrite license --plan 18 | -------------------------------------------------------------------------------- /.github/workflows/issue-comment-triage.yml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT - This GitHub Workflow is managed by automation 2 | # https://github.com/hashicorp/terraform-devex-repos 3 | name: Issue Comment Triage 4 | 5 | on: 6 | issue_comment: 7 | types: [created] 8 | 9 | jobs: 10 | issue_comment_triage: 11 | runs-on: ubuntu-latest 12 | env: 13 | # issue_comment events are triggered by comments on issues and pull requests. Checking the 14 | # value of github.event.issue.pull_request tells us whether the issue is an issue or is 15 | # actually a pull request, allowing us to dynamically set the gh subcommand: 16 | # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment-on-issues-only-or-pull-requests-only 17 | COMMAND: ${{ github.event.issue.pull_request && 'pr' || 'issue' }} 18 | GH_TOKEN: ${{ github.token }} 19 | steps: 20 | - name: 'Remove waiting-response on comment' 21 | run: gh ${{ env.COMMAND }} edit ${{ github.event.issue.html_url }} --remove-label waiting-response 22 | -------------------------------------------------------------------------------- /.github/workflows/lock.yml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT - This GitHub Workflow is managed by automation 2 | # https://github.com/hashicorp/terraform-devex-repos 3 | name: 'Lock Threads' 4 | 5 | on: 6 | schedule: 7 | - cron: '45 17 * * *' 8 | 9 | jobs: 10 | lock: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # NOTE: When TSCCR updates the GitHub action version, update the template workflow file to avoid drift: 14 | # https://github.com/hashicorp/terraform-devex-repos/blob/main/modules/repo/workflows/lock.tftpl 15 | - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 16 | with: 17 | github-token: ${{ github.token }} 18 | issue-inactive-days: '30' 19 | issue-lock-reason: resolved 20 | pr-inactive-days: '30' 21 | pr-lock-reason: resolved 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | website-preview 3 | 4 | # Jetbrains IDEs 5 | .idea/ 6 | *.iws 7 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - copyloopvar 6 | - durationcheck 7 | - errcheck 8 | - govet 9 | - ineffassign 10 | - makezero 11 | - nilerr 12 | - predeclared 13 | - staticcheck 14 | - unconvert 15 | - unparam 16 | - unused 17 | - usetesting 18 | exclusions: 19 | generated: lax 20 | presets: 21 | - comments 22 | - common-false-positives 23 | - legacy 24 | - std-error-handling 25 | rules: 26 | - linters: 27 | - staticcheck 28 | text: 'SA1019: schema.SchemaValidateFunc is deprecated' 29 | paths: 30 | - third_party$ 31 | - builtin$ 32 | - examples$ 33 | settings: 34 | staticcheck: 35 | checks: 36 | - all 37 | - '-QF1001' # "could apply De Morgan's law" -- https://staticcheck.dev/docs/checks/#QF1001 38 | - '-QF1002' # "could use tagged switch" -- https://staticcheck.dev/docs/checks/#QF1002 39 | - '-QF1004' # "could use strings.ReplaceAll instead" -- https://staticcheck.dev/docs/checks/#QF1004 40 | - '-QF1007' # "could merge conditional assignment into variable declaration" -- https://staticcheck.dev/docs/checks/#QF1007 41 | - '-QF1008' # "could remove embedded field "Block" from selector" -- https://staticcheck.dev/docs/checks/#QF1008 42 | - '-QF1011' # "could omit type *terraform.InstanceState from declaration" -- https://staticcheck.dev/docs/checks/#QF1011 43 | - '-ST1003' # example: "const autoTFVarsJson should be autoTFVarsJSON" -- https://staticcheck.dev/docs/checks/#ST1003 44 | - '-ST1005' # "error strings should not end with punctuation or newlines" -- https://staticcheck.dev/docs/checks/#ST1005 45 | - '-ST1016' # example: "methods on the same type should have the same receiver name (seen 2x "r", 2x "s")" -- https://staticcheck.dev/docs/checks/#ST1016 46 | - '-ST1023' # example: "should omit type *terraform.InstanceState from declaration;" -- https://staticcheck.dev/docs/checks/#ST1023 47 | issues: 48 | max-issues-per-linter: 0 49 | max-same-issues: 0 50 | formatters: 51 | enable: 52 | - gofmt 53 | exclusions: 54 | generated: lax 55 | paths: 56 | - third_party$ 57 | - builtin$ 58 | - examples$ 59 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | project_name: terraform-plugin-sdk 3 | builds: 4 | - skip: true 5 | milestones: 6 | - close: true 7 | release: 8 | prerelease: auto 9 | ids: 10 | - 'none' 11 | -------------------------------------------------------------------------------- /META.d/_summary.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | schema: 1.1 3 | 4 | partition: tf-ecosystem 5 | 6 | summary: 7 | owner: team-tf-core-plugins 8 | description: | 9 | Terraform Plugin SDK enables building plugins (providers) to manage any service providers or custom in-house solutions 10 | visibility: public 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOFMT_FILES?=$$(find . -name '*.go') 2 | 3 | default: test 4 | 5 | test: generate 6 | go test ./... 7 | 8 | lint: 9 | golangci-lint run 10 | 11 | generate: 12 | go generate ./... 13 | cd tools; go generate ./... 14 | 15 | fmt: 16 | gofmt -s -w -e $(GOFMT_FILES) 17 | 18 | # Run this if working on the website locally to run in watch mode. 19 | website: 20 | $(MAKE) -C website website 21 | 22 | # Use this if you have run `website/build-local` to use the locally built image. 23 | website/local: 24 | $(MAKE) -C website website/local 25 | 26 | # Run this to generate a new local Docker image. 27 | website/build-local: 28 | $(MAKE) -C website website/build-local 29 | 30 | .PHONY: default fmt lint generate test website website/local website/build-local 31 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Q3 2021 Roadmap 2 | 3 | Each quarter, the team will highlight areas of focus for our work and upcoming 4 | research. 5 | 6 | Each release will include necessary tasks that lead to the completion of the 7 | stated goals as well as community pull requests, enhancements, and features 8 | that are not highlighted in the roadmap. This upcoming calendar quarter 9 | (Aug-Oct 2021) we will be prioritizing the following areas of work: 10 | 11 | ## Currently In Progress 12 | 13 | ### v2.x Release 14 | 15 | Following the release of v2.0.0 we will continue to make improvements to the 16 | SDK to ease the upgrade path for provider developers who have not yet migrated 17 | and improve the functionality for those that have. 18 | 19 | ### terraform-plugin-go / terraform-plugin-mux improvements 20 | 21 | Following the release of the 22 | [terraform-plugin-go](https://github.com/hashicorp/terraform-plugin-go) and 23 | [terraform-plugin-mux](https://github.com/hashicorp/terraform-plugin-mux) modules, we 24 | will work to improve and refine these modules to make them more robust, easier 25 | to use, and more approachable for developers. This includes creating [new 26 | abstractions](https://github.com/hashicorp/terraform-plugin-go-contrib) for 27 | terraform-plugin-go that developers may find useful and will help inform our 28 | future design efforts. 29 | 30 | ### An HCL2-native framework 31 | 32 | Following the release of 33 | [terraform-plugin-framework](https://github.com/hashicorp/terraform-plugin-framework) 34 | we will work to add more features to the framework, aiming for feature parity with 35 | SDKv2, and work to make the framework more approachable, easier to use, and better 36 | documented. 37 | 38 | ### More observable provider development 39 | 40 | With the release of 41 | [terraform-plugin-log](https://github.com/hashicorp/terraform-plugin-log) we will work 42 | to integrate this tooling into terraform-plugin-go, terraform-plugin-framework, and v2 43 | of the SDK to help enable more powerful debugging of Terraform providers. 44 | 45 | ## Under Research 46 | 47 | ### A modernized testing framework 48 | 49 | Following the creation of our reattach-based binary test driver in v2.0.0 of 50 | the SDK, we're investigating what a redesigned approach to testing Terraform 51 | providers may look like and if we can aid developers in making and testing 52 | clearer assertions about their providers. 53 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support Policy 2 | 3 | Version 2 of the Terraform Plugin SDK is considered **GA (generally 4 | available)** and has the following support policy: 5 | 6 | - Critical bug fixes will be accepted and merged. New features and non-critical 7 | bug fixes may be accepted at maintainers’ discretion, but our priority is 8 | maintaining stability. 9 | - We will not break the public interface exposed by the SDK in a minor or patch 10 | release unless a critical security issue or bug demands it, and only then as a 11 | last resort. 12 | - We will continue testing version 2 of the SDK against new releases of Terraform 13 | to ensure compatibility. 14 | - We will not be backporting bug fixes to prior minor releases. Only the latest 15 | minor release receives new patch versions. 16 | 17 | # Version 1 18 | 19 | Please see the 20 | [v1-maint](https://github.com/hashicorp/terraform-plugin-sdk/blob/v1-maint/SUPPORT.md) 21 | branch for the support policy of version 1 of the SDK. 22 | -------------------------------------------------------------------------------- /diag/helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package diag 5 | 6 | import "fmt" 7 | 8 | // FromErr will convert an error into a Diagnostics. This returns Diagnostics 9 | // as the most common use case in Go will be handling a single error 10 | // returned from a function. 11 | // 12 | // if err != nil { 13 | // return diag.FromErr(err) 14 | // } 15 | func FromErr(err error) Diagnostics { 16 | if err == nil { 17 | return nil 18 | } 19 | return Diagnostics{ 20 | Diagnostic{ 21 | Severity: Error, 22 | Summary: err.Error(), 23 | }, 24 | } 25 | } 26 | 27 | // Errorf creates a Diagnostics with a single Error level Diagnostic entry. 28 | // The summary is populated by performing a fmt.Sprintf with the supplied 29 | // values. This returns a single error in a Diagnostics as errors typically 30 | // do not occur in multiples as warnings may. 31 | // 32 | // if unexpectedCondition { 33 | // return diag.Errorf("unexpected: %s", someValue) 34 | // } 35 | func Errorf(format string, a ...interface{}) Diagnostics { 36 | return Diagnostics{ 37 | Diagnostic{ 38 | Severity: Error, 39 | Summary: fmt.Sprintf(format, a...), 40 | }, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/terraform-plugin-sdk/v2 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.7 6 | 7 | require ( 8 | github.com/google/go-cmp v0.7.0 9 | github.com/hashicorp/go-cty v1.5.0 10 | github.com/hashicorp/go-hclog v1.6.3 11 | github.com/hashicorp/go-plugin v1.6.3 12 | github.com/hashicorp/go-uuid v1.0.3 13 | github.com/hashicorp/go-version v1.7.0 14 | github.com/hashicorp/hc-install v0.9.2 15 | github.com/hashicorp/hcl/v2 v2.23.0 16 | github.com/hashicorp/logutils v1.0.0 17 | github.com/hashicorp/terraform-exec v0.23.0 18 | github.com/hashicorp/terraform-json v0.25.0 19 | github.com/hashicorp/terraform-plugin-go v0.28.0 20 | github.com/hashicorp/terraform-plugin-log v0.9.0 21 | github.com/mitchellh/copystructure v1.2.0 22 | github.com/mitchellh/go-testing-interface v1.14.1 23 | github.com/mitchellh/mapstructure v1.5.0 24 | github.com/mitchellh/reflectwalk v1.0.2 25 | github.com/zclconf/go-cty v1.16.3 26 | golang.org/x/crypto v0.39.0 27 | ) 28 | 29 | require ( 30 | github.com/ProtonMail/go-crypto v1.1.6 // indirect 31 | github.com/agext/levenshtein v1.2.2 // indirect 32 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 33 | github.com/cloudflare/circl v1.6.0 // indirect 34 | github.com/fatih/color v1.16.0 // indirect 35 | github.com/golang/protobuf v1.5.4 // indirect 36 | github.com/hashicorp/errwrap v1.0.0 // indirect 37 | github.com/hashicorp/go-checkpoint v0.5.0 // indirect 38 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 39 | github.com/hashicorp/go-multierror v1.1.1 // indirect 40 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 41 | github.com/hashicorp/terraform-registry-address v0.2.5 // indirect 42 | github.com/hashicorp/terraform-svchost v0.1.1 // indirect 43 | github.com/hashicorp/yamux v0.1.1 // indirect 44 | github.com/mattn/go-colorable v0.1.13 // indirect 45 | github.com/mattn/go-isatty v0.0.20 // indirect 46 | github.com/mitchellh/go-wordwrap v1.0.0 // indirect 47 | github.com/oklog/run v1.0.0 // indirect 48 | github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect 49 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 50 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 51 | golang.org/x/mod v0.25.0 // indirect 52 | golang.org/x/net v0.40.0 // indirect 53 | golang.org/x/sync v0.15.0 // indirect 54 | golang.org/x/sys v0.33.0 // indirect 55 | golang.org/x/text v0.26.0 // indirect 56 | golang.org/x/tools v0.33.0 // indirect 57 | google.golang.org/appengine v1.6.8 // indirect 58 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect 59 | google.golang.org/grpc v1.72.1 // indirect 60 | google.golang.org/protobuf v1.36.6 // indirect 61 | ) 62 | -------------------------------------------------------------------------------- /helper/customdiff/compose.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package customdiff 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 11 | ) 12 | 13 | // All returns a CustomizeDiffFunc that runs all of the given 14 | // CustomizeDiffFuncs and returns all of the errors produced. 15 | // 16 | // If one function produces an error, functions after it are still run. 17 | // If this is not desirable, use function Sequence instead. 18 | // 19 | // If multiple functions returns errors, the result is a multierror. 20 | // 21 | // For example: 22 | // 23 | // &schema.Resource{ 24 | // // ... 25 | // CustomizeDiff: customdiff.All( 26 | // customdiff.ValidateChange("size", func (ctx context.Context, old, new, meta interface{}) error { 27 | // // If we are increasing "size" then the new value must be 28 | // // a multiple of the old value. 29 | // if new.(int) <= old.(int) { 30 | // return nil 31 | // } 32 | // if (new.(int) % old.(int)) != 0 { 33 | // return fmt.Errorf("new size value must be an integer multiple of old value %d", old.(int)) 34 | // } 35 | // return nil 36 | // }), 37 | // customdiff.ForceNewIfChange("size", func (ctx context.Context, old, new, meta interface{}) bool { 38 | // // "size" can only increase in-place, so we must create a new resource 39 | // // if it is decreased. 40 | // return new.(int) < old.(int) 41 | // }), 42 | // customdiff.ComputedIf("version_id", func (ctx context.Context, d *schema.ResourceDiff, meta interface{}) bool { 43 | // // Any change to "content" causes a new "version_id" to be allocated. 44 | // return d.HasChange("content") 45 | // }), 46 | // ), 47 | // } 48 | func All(funcs ...schema.CustomizeDiffFunc) schema.CustomizeDiffFunc { 49 | return func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error { 50 | var errs []error 51 | for _, f := range funcs { 52 | thisErr := f(ctx, d, meta) 53 | if thisErr != nil { 54 | errs = append(errs, thisErr) 55 | } 56 | } 57 | return errors.Join(errs...) 58 | } 59 | } 60 | 61 | // Sequence returns a CustomizeDiffFunc that runs all of the given 62 | // CustomizeDiffFuncs in sequence, stopping at the first one that returns 63 | // an error and returning that error. 64 | // 65 | // If all functions succeed, the combined function also succeeds. 66 | func Sequence(funcs ...schema.CustomizeDiffFunc) schema.CustomizeDiffFunc { 67 | return func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error { 68 | for _, f := range funcs { 69 | err := f(ctx, d, meta) 70 | if err != nil { 71 | return err 72 | } 73 | } 74 | return nil 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /helper/customdiff/compose_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package customdiff 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "testing" 10 | 11 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 12 | ) 13 | 14 | func TestAll(t *testing.T) { 15 | var aCalled, bCalled, cCalled bool 16 | aErr := errors.New("A bad") 17 | cErr := errors.New("C bad") 18 | 19 | provider := testProvider( 20 | map[string]*schema.Schema{}, 21 | All( 22 | func(_ context.Context, d *schema.ResourceDiff, meta interface{}) error { 23 | aCalled = true 24 | return aErr 25 | }, 26 | func(_ context.Context, d *schema.ResourceDiff, meta interface{}) error { 27 | bCalled = true 28 | return nil 29 | }, 30 | func(_ context.Context, d *schema.ResourceDiff, meta interface{}) error { 31 | cCalled = true 32 | return cErr 33 | }, 34 | ), 35 | ) 36 | 37 | _, err := testDiff( 38 | provider, 39 | map[string]string{ 40 | "foo": "bar", 41 | }, 42 | map[string]string{ 43 | "foo": "baz", 44 | }, 45 | ) 46 | 47 | if err == nil { 48 | t.Fatal("Diff succeeded; want error") 49 | } 50 | if !errors.Is(err, aErr) { 51 | t.Errorf("Missing substring %q in error message %q", aErr, err) 52 | } 53 | if !errors.Is(err, cErr) { 54 | t.Errorf("Missing substring %q in error message %q", cErr, err) 55 | } 56 | 57 | if !aCalled { 58 | t.Error("customize callback A was not called") 59 | } 60 | if !bCalled { 61 | t.Error("customize callback B was not called") 62 | } 63 | if !cCalled { 64 | t.Error("customize callback C was not called") 65 | } 66 | } 67 | 68 | func TestSequence(t *testing.T) { 69 | var aCalled, bCalled, cCalled bool 70 | 71 | provider := testProvider( 72 | map[string]*schema.Schema{}, 73 | Sequence( 74 | func(_ context.Context, d *schema.ResourceDiff, meta interface{}) error { 75 | aCalled = true 76 | return nil 77 | }, 78 | func(_ context.Context, d *schema.ResourceDiff, meta interface{}) error { 79 | bCalled = true 80 | return errors.New("B bad") 81 | }, 82 | func(_ context.Context, d *schema.ResourceDiff, meta interface{}) error { 83 | cCalled = true 84 | return errors.New("C bad") 85 | }, 86 | ), 87 | ) 88 | 89 | _, err := testDiff( 90 | provider, 91 | map[string]string{ 92 | "foo": "bar", 93 | }, 94 | map[string]string{ 95 | "foo": "baz", 96 | }, 97 | ) 98 | 99 | if err == nil { 100 | t.Fatal("Diff succeeded; want error") 101 | } 102 | if got, want := err.Error(), "B bad"; got != want { 103 | t.Errorf("Wrong error message %q; want %q", got, want) 104 | } 105 | 106 | if !aCalled { 107 | t.Error("customize callback A was not called") 108 | } 109 | if !bCalled { 110 | t.Error("customize callback B was not called") 111 | } 112 | if cCalled { 113 | t.Error("customize callback C was called (should not have been)") 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /helper/customdiff/computed.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package customdiff 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging" 11 | ) 12 | 13 | // ComputedIf returns a CustomizeDiffFunc that sets the given key's new value 14 | // as computed if the given condition function returns true. 15 | // 16 | // This function is best effort and will generate a warning log on any errors. 17 | func ComputedIf(key string, f ResourceConditionFunc) schema.CustomizeDiffFunc { 18 | return func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error { 19 | if f(ctx, d, meta) { 20 | // To prevent backwards compatibility issues, this logic only 21 | // generates a warning log instead of returning the error to 22 | // the provider and ultimately the practitioner. Providers may 23 | // not be aware of all situations in which the key may not be 24 | // present in the data, such as during resource creation, so any 25 | // further changes here should take that into account by 26 | // documenting how to prevent the error. 27 | if err := d.SetNewComputed(key); err != nil { 28 | logging.HelperSchemaWarn(ctx, "unable to set attribute value to unknown", map[string]interface{}{ 29 | logging.KeyAttributePath: key, 30 | logging.KeyError: err, 31 | }) 32 | } 33 | } 34 | return nil 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /helper/customdiff/condition.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package customdiff 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | ) 11 | 12 | // ResourceConditionFunc is a function type that makes a boolean decision based 13 | // on an entire resource diff. 14 | type ResourceConditionFunc func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) bool 15 | 16 | // ValueChangeConditionFunc is a function type that makes a boolean decision 17 | // by comparing two values. 18 | type ValueChangeConditionFunc func(ctx context.Context, oldValue, newValue, meta interface{}) bool 19 | 20 | // ValueConditionFunc is a function type that makes a boolean decision based 21 | // on a given value. 22 | type ValueConditionFunc func(ctx context.Context, value, meta interface{}) bool 23 | 24 | // If returns a CustomizeDiffFunc that calls the given condition 25 | // function and then calls the given CustomizeDiffFunc only if the condition 26 | // function returns true. 27 | // 28 | // This can be used to include conditional customizations when composing 29 | // customizations using All and Sequence, but should generally be used only in 30 | // simple scenarios. Prefer directly writing a CustomizeDiffFunc containing 31 | // a conditional branch if the given CustomizeDiffFunc is already a 32 | // locally-defined function, since this avoids obscuring the control flow. 33 | func If(cond ResourceConditionFunc, f schema.CustomizeDiffFunc) schema.CustomizeDiffFunc { 34 | return func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error { 35 | if cond(ctx, d, meta) { 36 | return f(ctx, d, meta) 37 | } 38 | return nil 39 | } 40 | } 41 | 42 | // IfValueChange returns a CustomizeDiffFunc that calls the given condition 43 | // function with the old and new values of the given key and then calls the 44 | // given CustomizeDiffFunc only if the condition function returns true. 45 | func IfValueChange(key string, cond ValueChangeConditionFunc, f schema.CustomizeDiffFunc) schema.CustomizeDiffFunc { 46 | return func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error { 47 | oldValue, newValue := d.GetChange(key) 48 | if cond(ctx, oldValue, newValue, meta) { 49 | return f(ctx, d, meta) 50 | } 51 | return nil 52 | } 53 | } 54 | 55 | // IfValue returns a CustomizeDiffFunc that calls the given condition 56 | // function with the new values of the given key and then calls the 57 | // given CustomizeDiffFunc only if the condition function returns true. 58 | func IfValue(key string, cond ValueConditionFunc, f schema.CustomizeDiffFunc) schema.CustomizeDiffFunc { 59 | return func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error { 60 | if cond(ctx, d.Get(key), meta) { 61 | return f(ctx, d, meta) 62 | } 63 | return nil 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /helper/customdiff/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package customdiff provides a set of reusable and composable functions 5 | // to enable more "declarative" use of the CustomizeDiff mechanism available 6 | // for resources in package helper/schema. 7 | // 8 | // The intent of these helpers is to make the intent of a set of diff 9 | // customizations easier to see, rather than lost in a sea of Go function 10 | // boilerplate. They should _not_ be used in situations where they _obscure_ 11 | // intent, e.g. by over-using the composition functions where a single 12 | // function containing normal Go control flow statements would be more 13 | // straightforward. 14 | package customdiff 15 | -------------------------------------------------------------------------------- /helper/customdiff/force_new.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package customdiff 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging" 11 | ) 12 | 13 | // ForceNewIf returns a CustomizeDiffFunc that flags the given key as 14 | // requiring a new resource if the given condition function returns true. 15 | // 16 | // The return value of the condition function is ignored if the old and new 17 | // values of the field compare equal, since no attribute diff is generated in 18 | // that case. 19 | // 20 | // This function is best effort and will generate a warning log on any errors. 21 | func ForceNewIf(key string, f ResourceConditionFunc) schema.CustomizeDiffFunc { 22 | return func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error { 23 | if f(ctx, d, meta) { 24 | // To prevent backwards compatibility issues, this logic only 25 | // generates a warning log instead of returning the error to 26 | // the provider and ultimately the practitioner. Providers may 27 | // not be aware of all situations in which the key may not be 28 | // present in the data, such as during resource creation, so any 29 | // further changes here should take that into account by 30 | // documenting how to prevent the error. 31 | if err := d.ForceNew(key); err != nil { 32 | logging.HelperSchemaWarn(ctx, "unable to require attribute replacement", map[string]interface{}{ 33 | logging.KeyAttributePath: key, 34 | logging.KeyError: err, 35 | }) 36 | } 37 | } 38 | return nil 39 | } 40 | } 41 | 42 | // ForceNewIfChange returns a CustomizeDiffFunc that flags the given key as 43 | // requiring a new resource if the given condition function returns true. 44 | // 45 | // The return value of the condition function is ignored if the old and new 46 | // values compare equal, since no attribute diff is generated in that case. 47 | // 48 | // This function is similar to ForceNewIf but provides the condition function 49 | // only the old and new values of the given key, which leads to more compact 50 | // and explicit code in the common case where the decision can be made with 51 | // only the specific field value. 52 | // 53 | // This function is best effort and will generate a warning log on any errors. 54 | func ForceNewIfChange(key string, f ValueChangeConditionFunc) schema.CustomizeDiffFunc { 55 | return func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error { 56 | oldValue, newValue := d.GetChange(key) 57 | if f(ctx, oldValue, newValue, meta) { 58 | // To prevent backwards compatibility issues, this logic only 59 | // generates a warning log instead of returning the error to 60 | // the provider and ultimately the practitioner. Providers may 61 | // not be aware of all situations in which the key may not be 62 | // present in the data, such as during resource creation, so any 63 | // further changes here should take that into account by 64 | // documenting how to prevent the error. 65 | if err := d.ForceNew(key); err != nil { 66 | logging.HelperSchemaWarn(ctx, "unable to require attribute replacement", map[string]interface{}{ 67 | logging.KeyAttributePath: key, 68 | logging.KeyError: err, 69 | }) 70 | } 71 | } 72 | return nil 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /helper/customdiff/testing_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package customdiff 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 11 | ) 12 | 13 | func testProvider(s map[string]*schema.Schema, cd schema.CustomizeDiffFunc) *schema.Provider { 14 | return &schema.Provider{ 15 | ResourcesMap: map[string]*schema.Resource{ 16 | "test": { 17 | Schema: s, 18 | CustomizeDiff: cd, 19 | }, 20 | }, 21 | } 22 | } 23 | 24 | func testDiff(provider *schema.Provider, oldValue, newValue map[string]string) (*terraform.InstanceDiff, error) { 25 | newI := make(map[string]interface{}, len(newValue)) 26 | for k, v := range newValue { 27 | newI[k] = v 28 | } 29 | 30 | return provider.ResourcesMap["test"].Diff( 31 | context.Background(), 32 | &terraform.InstanceState{ 33 | Attributes: oldValue, 34 | }, 35 | &terraform.ResourceConfig{ 36 | Config: newI, 37 | }, 38 | provider.Meta(), 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /helper/customdiff/validate.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package customdiff 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | ) 11 | 12 | // ValueChangeValidationFunc is a function type that validates the difference 13 | // (or lack thereof) between two values, returning an error if the change 14 | // is invalid. 15 | type ValueChangeValidationFunc func(ctx context.Context, oldValue, newValue, meta interface{}) error 16 | 17 | // ValueValidationFunc is a function type that validates a particular value, 18 | // returning an error if the value is invalid. 19 | type ValueValidationFunc func(ctx context.Context, value, meta interface{}) error 20 | 21 | // ValidateChange returns a CustomizeDiffFunc that applies the given validation 22 | // function to the change for the given key, returning any error produced. 23 | func ValidateChange(key string, f ValueChangeValidationFunc) schema.CustomizeDiffFunc { 24 | return func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error { 25 | oldValue, newValue := d.GetChange(key) 26 | return f(ctx, oldValue, newValue, meta) 27 | } 28 | } 29 | 30 | // ValidateValue returns a CustomizeDiffFunc that applies the given validation 31 | // function to value of the given key, returning any error produced. 32 | // 33 | // This should generally not be used since it is functionally equivalent to 34 | // a validation function applied directly to the schema attribute in question, 35 | // but is provided for situations where composing multiple CustomizeDiffFuncs 36 | // together makes intent clearer than spreading that validation across the 37 | // schema. 38 | func ValidateValue(key string, f ValueValidationFunc) schema.CustomizeDiffFunc { 39 | return func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error { 40 | val := d.Get(key) 41 | return f(ctx, val, meta) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /helper/customdiff/validate_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package customdiff 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "testing" 10 | 11 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 12 | ) 13 | 14 | func TestValidateChange(t *testing.T) { 15 | var called bool 16 | var gotOld, gotNew string 17 | 18 | provider := testProvider( 19 | map[string]*schema.Schema{ 20 | "foo": { 21 | Type: schema.TypeString, 22 | Optional: true, 23 | }, 24 | }, 25 | ValidateChange("foo", func(_ context.Context, oldValue, newValue, meta interface{}) error { 26 | called = true 27 | gotOld = oldValue.(string) 28 | gotNew = newValue.(string) 29 | return errors.New("bad") 30 | }), 31 | ) 32 | 33 | _, err := testDiff( 34 | provider, 35 | map[string]string{ 36 | "foo": "bar", 37 | }, 38 | map[string]string{ 39 | "foo": "baz", 40 | }, 41 | ) 42 | 43 | if err == nil { 44 | t.Fatalf("Diff succeeded; want error") 45 | } 46 | if got, want := err.Error(), "bad"; got != want { 47 | t.Fatalf("wrong error message %q; want %q", got, want) 48 | } 49 | 50 | if !called { 51 | t.Fatal("ValidateChange callback was not called") 52 | } 53 | if got, want := gotOld, "bar"; got != want { 54 | t.Errorf("wrong old value %q; want %q", got, want) 55 | } 56 | if got, want := gotNew, "baz"; got != want { 57 | t.Errorf("wrong new value %q; want %q", got, want) 58 | } 59 | } 60 | 61 | func TestValidateValue(t *testing.T) { 62 | var called bool 63 | var gotValue string 64 | 65 | provider := testProvider( 66 | map[string]*schema.Schema{ 67 | "foo": { 68 | Type: schema.TypeString, 69 | Optional: true, 70 | }, 71 | }, 72 | ValidateValue("foo", func(_ context.Context, value, meta interface{}) error { 73 | called = true 74 | gotValue = value.(string) 75 | return errors.New("bad") 76 | }), 77 | ) 78 | 79 | _, err := testDiff( 80 | provider, 81 | map[string]string{ 82 | "foo": "bar", 83 | }, 84 | map[string]string{ 85 | "foo": "baz", 86 | }, 87 | ) 88 | 89 | if err == nil { 90 | t.Fatalf("Diff succeeded; want error") 91 | } 92 | if got, want := err.Error(), "bad"; got != want { 93 | t.Fatalf("wrong error message %q; want %q", got, want) 94 | } 95 | 96 | if !called { 97 | t.Fatal("ValidateValue callback was not called") 98 | } 99 | if got, want := gotValue, "baz"; got != want { 100 | t.Errorf("wrong value %q; want %q", got, want) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /helper/id/id.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package id 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | const UniqueIdPrefix = `terraform-` 14 | 15 | // idCounter is a monotonic counter for generating ordered unique ids. 16 | var idMutex sync.Mutex 17 | var idCounter uint32 18 | 19 | // Helper for a resource to generate a unique identifier w/ default prefix 20 | func UniqueId() string { 21 | return PrefixedUniqueId(UniqueIdPrefix) 22 | } 23 | 24 | // UniqueIDSuffixLength is the string length of the suffix generated by 25 | // PrefixedUniqueId. This can be used by length validation functions to 26 | // ensure prefixes are the correct length for the target field. 27 | const UniqueIDSuffixLength = 26 28 | 29 | // Helper for a resource to generate a unique identifier w/ given prefix 30 | // 31 | // After the prefix, the ID consists of an incrementing 26 digit value (to match 32 | // previous timestamp output). After the prefix, the ID consists of a timestamp 33 | // and an incrementing 8 hex digit value The timestamp means that multiple IDs 34 | // created with the same prefix will sort in the order of their creation, even 35 | // across multiple terraform executions, as long as the clock is not turned back 36 | // between calls, and as long as any given terraform execution generates fewer 37 | // than 4 billion IDs. 38 | func PrefixedUniqueId(prefix string) string { 39 | // Be precise to 4 digits of fractional seconds, but remove the dot before the 40 | // fractional seconds. 41 | timestamp := strings.Replace( 42 | time.Now().UTC().Format("20060102150405.0000"), ".", "", 1) 43 | 44 | idMutex.Lock() 45 | defer idMutex.Unlock() 46 | idCounter++ 47 | return fmt.Sprintf("%s%s%08x", prefix, timestamp, idCounter) 48 | } 49 | -------------------------------------------------------------------------------- /helper/id/id_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package id 5 | 6 | import ( 7 | "regexp" 8 | "strings" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | var allDigits = regexp.MustCompile(`^\d+$`) 14 | var allHex = regexp.MustCompile(`^[a-f0-9]+$`) 15 | 16 | func TestUniqueId(t *testing.T) { 17 | split := func(rest string) (timestamp, increment string) { 18 | return rest[:18], rest[18:] 19 | } 20 | 21 | iterations := 10000 22 | ids := make(map[string]struct{}) 23 | var id, lastId string 24 | for i := 0; i < iterations; i++ { 25 | id = UniqueId() 26 | 27 | if _, ok := ids[id]; ok { 28 | t.Fatalf("Got duplicated id! %s", id) 29 | } 30 | 31 | if !strings.HasPrefix(id, UniqueIdPrefix) { 32 | t.Fatalf("Unique ID didn't have terraform- prefix! %s", id) 33 | } 34 | 35 | rest := strings.TrimPrefix(id, UniqueIdPrefix) 36 | 37 | if len(rest) != UniqueIDSuffixLength { 38 | t.Fatalf("PrefixedUniqueId is out of sync with UniqueIDSuffixLength, post-prefix part has wrong length! %s", rest) 39 | } 40 | 41 | timestamp, increment := split(rest) 42 | 43 | if !allDigits.MatchString(timestamp) { 44 | t.Fatalf("Timestamp not all digits! %s", timestamp) 45 | } 46 | 47 | if !allHex.MatchString(increment) { 48 | t.Fatalf("Increment part not all hex! %s", increment) 49 | } 50 | 51 | if lastId != "" && lastId >= id { 52 | t.Fatalf("IDs not ordered! %s vs %s", lastId, id) 53 | } 54 | 55 | ids[id] = struct{}{} 56 | lastId = id 57 | } 58 | 59 | id1 := UniqueId() 60 | time.Sleep(time.Millisecond) 61 | id2 := UniqueId() 62 | timestamp1, _ := split(strings.TrimPrefix(id1, UniqueIdPrefix)) 63 | timestamp2, _ := split(strings.TrimPrefix(id2, UniqueIdPrefix)) 64 | 65 | if timestamp1 == timestamp2 { 66 | t.Fatalf("Timestamp part should update at least once a millisecond %s %s", 67 | id1, id2) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /helper/logging/transport.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package logging 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "log" 10 | "net/http" 11 | "net/http/httputil" 12 | "strings" 13 | ) 14 | 15 | type transport struct { 16 | name string 17 | transport http.RoundTripper 18 | } 19 | 20 | func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { 21 | if IsDebugOrHigher() { 22 | reqData, err := httputil.DumpRequestOut(req, true) 23 | if err == nil { 24 | log.Printf("[DEBUG] "+logReqMsg, t.name, prettyPrintJsonLines(reqData)) 25 | } else { 26 | log.Printf("[ERROR] %s API Request error: %#v", t.name, err) 27 | } 28 | } 29 | 30 | resp, err := t.transport.RoundTrip(req) 31 | if err != nil { 32 | return resp, err 33 | } 34 | 35 | if IsDebugOrHigher() { 36 | respData, err := httputil.DumpResponse(resp, true) 37 | if err == nil { 38 | log.Printf("[DEBUG] "+logRespMsg, t.name, prettyPrintJsonLines(respData)) 39 | } else { 40 | log.Printf("[ERROR] %s API Response error: %#v", t.name, err) 41 | } 42 | } 43 | 44 | return resp, nil 45 | } 46 | 47 | // NewTransport creates a wrapper around a *http.RoundTripper, 48 | // designed to be used for the `Transport` field of http.Client. 49 | // 50 | // This logs each pair of HTTP request/response that it handles. 51 | // The logging is done via Go standard library `log` package. 52 | // 53 | // Deprecated: This will log the content of every http request/response 54 | // at `[DEBUG]` level, without any filtering. Any sensitive information 55 | // will appear as-is in your logs. Please use NewSubsystemLoggingHTTPTransport instead. 56 | func NewTransport(name string, t http.RoundTripper) *transport { 57 | return &transport{name, t} 58 | } 59 | 60 | // prettyPrintJsonLines iterates through a []byte line-by-line, 61 | // transforming any lines that are complete json into pretty-printed json. 62 | func prettyPrintJsonLines(b []byte) string { 63 | parts := strings.Split(string(b), "\n") 64 | for i, p := range parts { 65 | if b := []byte(p); json.Valid(b) { 66 | var out bytes.Buffer 67 | _ = json.Indent(&out, b, "", " ") // already checked for validity 68 | parts[i] = out.String() 69 | } 70 | } 71 | return strings.Join(parts, "\n") 72 | } 73 | 74 | const logReqMsg = `%s API Request Details: 75 | ---[ REQUEST ]--------------------------------------- 76 | %s 77 | -----------------------------------------------------` 78 | 79 | const logRespMsg = `%s API Response Details: 80 | ---[ RESPONSE ]-------------------------------------- 81 | %s 82 | -----------------------------------------------------` 83 | -------------------------------------------------------------------------------- /helper/resource/environment_variables.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package resource 5 | 6 | // Environment variables for acceptance testing. Additional environment 7 | // variable constants can be found in the internal/plugintest package. 8 | const ( 9 | // Environment variable to enable acceptance tests using this package's 10 | // ParallelTest and Test functions whose TestCase does not enable the 11 | // IsUnitTest field. Defaults to disabled, in which each test will call 12 | // (*testing.T).Skip(). Can be set to any value to enable acceptance tests, 13 | // however "1" is conventional. 14 | EnvTfAcc = "TF_ACC" 15 | 16 | // Environment variable with hostname for the provider under acceptance 17 | // test. The hostname is the first portion of the full provider source 18 | // address, such as "example.com" in example.com/myorg/myprovider. Defaults 19 | // to "registry.terraform.io". 20 | // 21 | // Only required if any Terraform configuration set via the TestStep 22 | // type Config field includes a provider source, such as the terraform 23 | // configuration block required_providers attribute. 24 | EnvTfAccProviderHost = "TF_ACC_PROVIDER_HOST" 25 | 26 | // Environment variable with namespace for the provider under acceptance 27 | // test. The namespace is the second portion of the full provider source 28 | // address, such as "myorg" in registry.terraform.io/myorg/myprovider. 29 | // Defaults to "-" for Terraform 0.12-0.13 compatibility and "hashicorp". 30 | // 31 | // Only required if any Terraform configuration set via the TestStep 32 | // type Config field includes a provider source, such as the terraform 33 | // configuration block required_providers attribute. 34 | EnvTfAccProviderNamespace = "TF_ACC_PROVIDER_NAMESPACE" 35 | ) 36 | -------------------------------------------------------------------------------- /helper/resource/json.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package resource 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | ) 10 | 11 | func unmarshalJSON(data []byte, v interface{}) error { 12 | dec := json.NewDecoder(bytes.NewReader(data)) 13 | dec.UseNumber() 14 | return dec.Decode(v) 15 | } 16 | -------------------------------------------------------------------------------- /helper/resource/testcase_providers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package resource 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "strings" 10 | ) 11 | 12 | // providerConfig takes the list of providers in a TestCase and returns a 13 | // config with only empty provider blocks. This is useful for Import, where no 14 | // config is provided, but the providers must be defined. 15 | func (c TestCase) providerConfig(_ context.Context, skipProviderBlock bool) string { 16 | var providerBlocks, requiredProviderBlocks strings.Builder 17 | 18 | // [BF] The Providers field handling predates the logic being moved to this 19 | // method. It's not entirely clear to me at this time why this field 20 | // is being used and not the others, but leaving it here just in case 21 | // it does have a special purpose that wasn't being unit tested prior. 22 | for name := range c.Providers { 23 | providerBlocks.WriteString(fmt.Sprintf("provider %q {}\n", name)) 24 | } 25 | 26 | for name, externalProvider := range c.ExternalProviders { 27 | if !skipProviderBlock { 28 | providerBlocks.WriteString(fmt.Sprintf("provider %q {}\n", name)) 29 | } 30 | 31 | if externalProvider.Source == "" && externalProvider.VersionConstraint == "" { 32 | continue 33 | } 34 | 35 | requiredProviderBlocks.WriteString(fmt.Sprintf(" %s = {\n", name)) 36 | 37 | if externalProvider.Source != "" { 38 | requiredProviderBlocks.WriteString(fmt.Sprintf(" source = %q\n", externalProvider.Source)) 39 | } 40 | 41 | if externalProvider.VersionConstraint != "" { 42 | requiredProviderBlocks.WriteString(fmt.Sprintf(" version = %q\n", externalProvider.VersionConstraint)) 43 | } 44 | 45 | requiredProviderBlocks.WriteString(" }\n") 46 | } 47 | 48 | if requiredProviderBlocks.Len() > 0 { 49 | return fmt.Sprintf(` 50 | terraform { 51 | required_providers { 52 | %[1]s 53 | } 54 | } 55 | 56 | %[2]s 57 | `, strings.TrimSuffix(requiredProviderBlocks.String(), "\n"), providerBlocks.String()) 58 | } 59 | 60 | return providerBlocks.String() 61 | } 62 | -------------------------------------------------------------------------------- /helper/resource/testcase_validate.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package resource 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging" 11 | ) 12 | 13 | // hasProviders returns true if the TestCase has set any of the 14 | // ExternalProviders, ProtoV5ProviderFactories, ProtoV6ProviderFactories, 15 | // ProviderFactories, or Providers fields. 16 | func (c TestCase) hasProviders(_ context.Context) bool { 17 | if len(c.ExternalProviders) > 0 { 18 | return true 19 | } 20 | 21 | if len(c.ProtoV5ProviderFactories) > 0 { 22 | return true 23 | } 24 | 25 | if len(c.ProtoV6ProviderFactories) > 0 { 26 | return true 27 | } 28 | 29 | if len(c.ProviderFactories) > 0 { 30 | return true 31 | } 32 | 33 | if len(c.Providers) > 0 { 34 | return true 35 | } 36 | 37 | return false 38 | } 39 | 40 | // validate ensures the TestCase is valid based on the following criteria: 41 | // 42 | // - No overlapping ExternalProviders and Providers entries 43 | // - No overlapping ExternalProviders and ProviderFactories entries 44 | // - TestStep validations performed by the (TestStep).validate() method. 45 | func (c TestCase) validate(ctx context.Context) error { 46 | logging.HelperResourceTrace(ctx, "Validating TestCase") 47 | 48 | if len(c.Steps) == 0 { 49 | err := fmt.Errorf("TestCase missing Steps") 50 | logging.HelperResourceError(ctx, "TestCase validation error", map[string]interface{}{logging.KeyError: err}) 51 | return err 52 | } 53 | 54 | for name := range c.ExternalProviders { 55 | if _, ok := c.Providers[name]; ok { 56 | err := fmt.Errorf("TestCase provider %q set in both ExternalProviders and Providers", name) 57 | logging.HelperResourceError(ctx, "TestCase validation error", map[string]interface{}{logging.KeyError: err}) 58 | return err 59 | } 60 | 61 | if _, ok := c.ProviderFactories[name]; ok { 62 | err := fmt.Errorf("TestCase provider %q set in both ExternalProviders and ProviderFactories", name) 63 | logging.HelperResourceError(ctx, "TestCase validation error", map[string]interface{}{logging.KeyError: err}) 64 | return err 65 | } 66 | } 67 | 68 | testCaseHasProviders := c.hasProviders(ctx) 69 | 70 | for stepIndex, step := range c.Steps { 71 | stepNumber := stepIndex + 1 // Use 1-based index for humans 72 | stepValidateReq := testStepValidateRequest{ 73 | StepNumber: stepNumber, 74 | TestCaseHasProviders: testCaseHasProviders, 75 | } 76 | 77 | err := step.validate(ctx, stepValidateReq) 78 | 79 | if err != nil { 80 | err := fmt.Errorf("TestStep %d/%d validation error: %w", stepNumber, len(c.Steps), err) 81 | logging.HelperResourceError(ctx, "TestCase validation error", map[string]interface{}{logging.KeyError: err}) 82 | return err 83 | } 84 | } 85 | 86 | return nil 87 | } 88 | -------------------------------------------------------------------------------- /helper/resource/testing_config.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package resource 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging" 11 | "github.com/hashicorp/terraform-plugin-sdk/v2/internal/plugintest" 12 | ) 13 | 14 | func testStepTaint(ctx context.Context, step TestStep, wd *plugintest.WorkingDir) error { 15 | if len(step.Taint) == 0 { 16 | return nil 17 | } 18 | 19 | logging.HelperResourceTrace(ctx, fmt.Sprintf("Using TestStep Taint: %v", step.Taint)) 20 | 21 | for _, p := range step.Taint { 22 | err := wd.Taint(ctx, p) 23 | if err != nil { 24 | return fmt.Errorf("error tainting resource: %s", err) 25 | } 26 | } 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /helper/resource/testing_new_config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package resource 5 | 6 | import ( 7 | "regexp" 8 | "testing" 9 | ) 10 | 11 | func TestTest_TestStep_ExpectError_NewConfig(t *testing.T) { 12 | t.Parallel() 13 | 14 | Test(t, TestCase{ 15 | ExternalProviders: map[string]ExternalProvider{ 16 | "random": { 17 | Source: "registry.terraform.io/hashicorp/random", 18 | VersionConstraint: "3.4.3", 19 | }, 20 | }, 21 | Steps: []TestStep{ 22 | { 23 | Config: `resource "random_string" "one" { 24 | length = 2 25 | min_upper = 4 26 | }`, 27 | ExpectError: regexp.MustCompile(`Error: Invalid Attribute Value`), 28 | }, 29 | }, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /helper/resource/testing_new_refresh_state.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package resource 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | 10 | tfjson "github.com/hashicorp/terraform-json" 11 | "github.com/mitchellh/go-testing-interface" 12 | 13 | "github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging" 14 | "github.com/hashicorp/terraform-plugin-sdk/v2/internal/plugintest" 15 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 16 | ) 17 | 18 | func testStepNewRefreshState(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, step TestStep, providers *providerFactories) error { 19 | t.Helper() 20 | 21 | var err error 22 | // Explicitly ensure prior state exists before refresh. 23 | err = runProviderCommand(ctx, t, func() error { 24 | _, err = getState(ctx, t, wd) 25 | if err != nil { 26 | return err 27 | } 28 | return nil 29 | }, wd, providers) 30 | if err != nil { 31 | t.Fatalf("Error getting state: %s", err) 32 | } 33 | 34 | err = runProviderCommand(ctx, t, func() error { 35 | return wd.Refresh(ctx) 36 | }, wd, providers) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | var refreshState *terraform.State 42 | err = runProviderCommand(ctx, t, func() error { 43 | refreshState, err = getState(ctx, t, wd) 44 | if err != nil { 45 | return err 46 | } 47 | return nil 48 | }, wd, providers) 49 | if err != nil { 50 | t.Fatalf("Error getting state: %s", err) 51 | } 52 | 53 | // Go through the refreshed state and verify 54 | if step.Check != nil { 55 | logging.HelperResourceDebug(ctx, "Calling TestStep Check for RefreshState") 56 | 57 | if err := step.Check(refreshState); err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | logging.HelperResourceDebug(ctx, "Called TestStep Check for RefreshState") 62 | } 63 | 64 | // do a plan 65 | err = runProviderCommand(ctx, t, func() error { 66 | return wd.CreatePlan(ctx) 67 | }, wd, providers) 68 | if err != nil { 69 | return fmt.Errorf("Error running post-apply plan: %w", err) 70 | } 71 | 72 | var plan *tfjson.Plan 73 | err = runProviderCommand(ctx, t, func() error { 74 | var err error 75 | plan, err = wd.SavedPlan(ctx) 76 | return err 77 | }, wd, providers) 78 | if err != nil { 79 | return fmt.Errorf("Error retrieving post-apply plan: %w", err) 80 | } 81 | 82 | if !planIsEmpty(plan) && !step.ExpectNonEmptyPlan { 83 | var stdout string 84 | err = runProviderCommand(ctx, t, func() error { 85 | var err error 86 | stdout, err = wd.SavedPlanRawStdout(ctx) 87 | return err 88 | }, wd, providers) 89 | if err != nil { 90 | return fmt.Errorf("Error retrieving formatted plan output: %w", err) 91 | } 92 | return fmt.Errorf("After refreshing state during this test step, a followup plan was not empty.\nstdout:\n\n%s", stdout) 93 | } 94 | 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /helper/resource/teststep_providers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package resource 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "regexp" 10 | "strings" 11 | ) 12 | 13 | var configProviderBlockRegex = regexp.MustCompile(`provider "?[a-zA-Z0-9_-]+"? {`) 14 | 15 | // configHasProviderBlock returns true if the Config has declared a provider 16 | // configuration block, e.g. provider "examplecloud" {...} 17 | func (s TestStep) configHasProviderBlock(_ context.Context) bool { 18 | return configProviderBlockRegex.MatchString(s.Config) 19 | } 20 | 21 | // configHasTerraformBlock returns true if the Config has declared a terraform 22 | // configuration block, e.g. terraform {...} 23 | func (s TestStep) configHasTerraformBlock(_ context.Context) bool { 24 | return strings.Contains(s.Config, "terraform {") 25 | } 26 | 27 | // mergedConfig prepends any necessary terraform configuration blocks to the 28 | // TestStep Config. 29 | // 30 | // If there are ExternalProviders configurations in either the TestCase or 31 | // TestStep, the terraform configuration block should be included with the 32 | // step configuration to prevent errors with providers outside the 33 | // registry.terraform.io hostname or outside the hashicorp namespace. 34 | func (s TestStep) mergedConfig(ctx context.Context, testCase TestCase) string { 35 | var config strings.Builder 36 | 37 | // Prevent issues with existing configurations containing the terraform 38 | // configuration block. 39 | if s.configHasTerraformBlock(ctx) { 40 | config.WriteString(s.Config) 41 | 42 | return config.String() 43 | } 44 | 45 | if testCase.hasProviders(ctx) { 46 | config.WriteString(testCase.providerConfig(ctx, s.configHasProviderBlock(ctx))) 47 | } else { 48 | config.WriteString(s.providerConfig(ctx, s.configHasProviderBlock(ctx))) 49 | } 50 | 51 | config.WriteString(s.Config) 52 | 53 | return config.String() 54 | } 55 | 56 | // providerConfig takes the list of providers in a TestStep and returns a 57 | // config with only empty provider blocks. This is useful for Import, where no 58 | // config is provided, but the providers must be defined. 59 | func (s TestStep) providerConfig(_ context.Context, skipProviderBlock bool) string { 60 | var providerBlocks, requiredProviderBlocks strings.Builder 61 | 62 | for name, externalProvider := range s.ExternalProviders { 63 | if !skipProviderBlock { 64 | providerBlocks.WriteString(fmt.Sprintf("provider %q {}\n", name)) 65 | } 66 | 67 | if externalProvider.Source == "" && externalProvider.VersionConstraint == "" { 68 | continue 69 | } 70 | 71 | requiredProviderBlocks.WriteString(fmt.Sprintf(" %s = {\n", name)) 72 | 73 | if externalProvider.Source != "" { 74 | requiredProviderBlocks.WriteString(fmt.Sprintf(" source = %q\n", externalProvider.Source)) 75 | } 76 | 77 | if externalProvider.VersionConstraint != "" { 78 | requiredProviderBlocks.WriteString(fmt.Sprintf(" version = %q\n", externalProvider.VersionConstraint)) 79 | } 80 | 81 | requiredProviderBlocks.WriteString(" }\n") 82 | } 83 | 84 | if requiredProviderBlocks.Len() > 0 { 85 | return fmt.Sprintf(` 86 | terraform { 87 | required_providers { 88 | %[1]s 89 | } 90 | } 91 | 92 | %[2]s 93 | `, strings.TrimSuffix(requiredProviderBlocks.String(), "\n"), providerBlocks.String()) 94 | } 95 | 96 | return providerBlocks.String() 97 | } 98 | -------------------------------------------------------------------------------- /helper/retry/error.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package retry 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | type NotFoundError struct { 13 | LastError error 14 | LastRequest interface{} 15 | LastResponse interface{} 16 | Message string 17 | Retries int 18 | } 19 | 20 | func (e *NotFoundError) Error() string { 21 | if e.Message != "" { 22 | return e.Message 23 | } 24 | 25 | if e.Retries > 0 { 26 | return fmt.Sprintf("couldn't find resource (%d retries)", e.Retries) 27 | } 28 | 29 | return "couldn't find resource" 30 | } 31 | 32 | func (e *NotFoundError) Unwrap() error { 33 | return e.LastError 34 | } 35 | 36 | // UnexpectedStateError is returned when Refresh returns a state that's neither in Target nor Pending 37 | type UnexpectedStateError struct { 38 | LastError error 39 | State string 40 | ExpectedState []string 41 | } 42 | 43 | func (e *UnexpectedStateError) Error() string { 44 | return fmt.Sprintf( 45 | "unexpected state '%s', wanted target '%s'. last error: %s", 46 | e.State, 47 | strings.Join(e.ExpectedState, ", "), 48 | e.LastError, 49 | ) 50 | } 51 | 52 | func (e *UnexpectedStateError) Unwrap() error { 53 | return e.LastError 54 | } 55 | 56 | // TimeoutError is returned when WaitForState times out 57 | type TimeoutError struct { 58 | LastError error 59 | LastState string 60 | Timeout time.Duration 61 | ExpectedState []string 62 | } 63 | 64 | func (e *TimeoutError) Error() string { 65 | expectedState := "resource to be gone" 66 | if len(e.ExpectedState) > 0 { 67 | expectedState = fmt.Sprintf("state to become '%s'", strings.Join(e.ExpectedState, ", ")) 68 | } 69 | 70 | extraInfo := make([]string, 0) 71 | if e.LastState != "" { 72 | extraInfo = append(extraInfo, fmt.Sprintf("last state: '%s'", e.LastState)) 73 | } 74 | if e.Timeout > 0 { 75 | extraInfo = append(extraInfo, fmt.Sprintf("timeout: %s", e.Timeout.String())) 76 | } 77 | 78 | suffix := "" 79 | if len(extraInfo) > 0 { 80 | suffix = fmt.Sprintf(" (%s)", strings.Join(extraInfo, ", ")) 81 | } 82 | 83 | if e.LastError != nil { 84 | return fmt.Sprintf("timeout while waiting for %s%s: %s", 85 | expectedState, suffix, e.LastError) 86 | } 87 | 88 | return fmt.Sprintf("timeout while waiting for %s%s", 89 | expectedState, suffix) 90 | } 91 | 92 | func (e *TimeoutError) Unwrap() error { 93 | return e.LastError 94 | } 95 | -------------------------------------------------------------------------------- /helper/schema/README.md: -------------------------------------------------------------------------------- 1 | # Terraform Helper Lib: schema 2 | 3 | The `schema` package provides a high-level interface for writing resource 4 | providers for Terraform. 5 | 6 | If you're writing a resource provider, we recommend you use this package. 7 | 8 | The interface exposed by this package is much friendlier than trying to 9 | write to the Terraform API directly. The core Terraform API is low-level 10 | and built for maximum flexibility and control, whereas this library is built 11 | as a framework around that to more easily write common providers. 12 | -------------------------------------------------------------------------------- /helper/schema/context.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package schema 5 | 6 | type Key string 7 | 8 | var ( 9 | StopContextKey = Key("StopContext") 10 | ) 11 | -------------------------------------------------------------------------------- /helper/schema/data_source_resource_shim.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package schema 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | // DataSourceResourceShim takes a Resource instance describing a data source 11 | // (with a Read implementation and a Schema, at least) and returns a new 12 | // Resource instance with additional Create and Delete implementations that 13 | // allow the data source to be used as a resource. 14 | // 15 | // This is a backward-compatibility layer for data sources that were formerly 16 | // read-only resources before the data source concept was added. It should not 17 | // be used for any *new* data sources. 18 | // 19 | // The Read function for the data source *must* call d.SetId with a non-empty 20 | // id in order for this shim to function as expected. 21 | // 22 | // The provided Resource instance, and its schema, will be modified in-place 23 | // to make it suitable for use as a full resource. 24 | func DataSourceResourceShim(name string, dataSource *Resource) *Resource { 25 | // Recursively, in-place adjust the schema so that it has ForceNew 26 | // on any user-settable resource. 27 | dataSourceResourceShimAdjustSchema(dataSource.Schema) 28 | 29 | dataSource.Create = CreateFunc(dataSource.Read) 30 | dataSource.Delete = func(d *ResourceData, meta interface{}) error { 31 | d.SetId("") 32 | return nil 33 | } 34 | dataSource.Update = nil // should already be nil, but let's make sure 35 | 36 | // FIXME: Link to some further docs either on the website or in the 37 | // changelog, once such a thing exists. 38 | dataSource.DeprecationMessage = fmt.Sprintf( 39 | "using %s as a resource is deprecated; consider using the data source instead", 40 | name, 41 | ) 42 | 43 | return dataSource 44 | } 45 | 46 | func dataSourceResourceShimAdjustSchema(schema map[string]*Schema) { 47 | for _, s := range schema { 48 | // If the attribute is configurable then it must be ForceNew, 49 | // since we have no Update implementation. 50 | if s.Required || s.Optional { 51 | s.ForceNew = true 52 | } 53 | 54 | // If the attribute is a nested resource, we need to recursively 55 | // apply these same adjustments to it. 56 | if s.Elem != nil { 57 | if r, ok := s.Elem.(*Resource); ok { 58 | dataSourceResourceShimAdjustSchema(r.Schema) 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /helper/schema/deferred.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package schema 5 | 6 | // MAINTAINER NOTE: Only PROVIDER_CONFIG_UNKNOWN (enum value 2 in the plugin-protocol) is relevant 7 | // for SDKv2. Since (Deferred).Reason is mapped directly to the plugin-protocol, 8 | // the other enum values are intentionally omitted here. 9 | const ( 10 | // DeferredReasonUnknown is used to indicate an invalid `DeferredReason`. 11 | // Provider developers should not use it. 12 | DeferredReasonUnknown DeferredReason = 0 13 | 14 | // DeferredReasonProviderConfigUnknown represents a deferred reason caused 15 | // by unknown provider configuration. 16 | DeferredReasonProviderConfigUnknown DeferredReason = 2 17 | ) 18 | 19 | // Deferred is used to indicate to Terraform that a resource or data source is not able 20 | // to be applied yet and should be skipped (deferred). After completing an apply that has deferred actions, 21 | // the practitioner can then execute additional plan and apply “rounds” to eventually reach convergence 22 | // where there are no remaining deferred actions. 23 | // 24 | // NOTE: This functionality is related to deferred action support, which is currently experimental and is subject 25 | // to change or break without warning. It is not protected by version compatibility guarantees. 26 | type Deferred struct { 27 | // Reason represents the deferred reason. 28 | Reason DeferredReason 29 | } 30 | 31 | // DeferredReason represents different reasons for deferring a change. 32 | // 33 | // NOTE: This functionality is related to deferred action support, which is currently experimental and is subject 34 | // to change or break without warning. It is not protected by version compatibility guarantees. 35 | type DeferredReason int32 36 | 37 | func (d DeferredReason) String() string { 38 | switch d { 39 | case 0: 40 | return "Unknown" 41 | case 2: 42 | return "Provider Config Unknown" 43 | } 44 | return "Unknown" 45 | } 46 | -------------------------------------------------------------------------------- /helper/schema/equal.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package schema 5 | 6 | // Equal is an interface that checks for deep equality between two objects. 7 | type Equal interface { 8 | Equal(interface{}) bool 9 | } 10 | -------------------------------------------------------------------------------- /helper/schema/field_reader_map_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package schema 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestMapFieldReader_impl(t *testing.T) { 12 | var _ FieldReader = new(MapFieldReader) 13 | } 14 | 15 | func TestMapFieldReader(t *testing.T) { 16 | testFieldReader(t, func(s map[string]*Schema) FieldReader { 17 | return &MapFieldReader{ 18 | Schema: s, 19 | 20 | Map: BasicMapReader(map[string]string{ 21 | "bool": "true", 22 | "int": "42", 23 | "float": "3.1415", 24 | "string": "string", 25 | 26 | "list.#": "2", 27 | "list.0": "foo", 28 | "list.1": "bar", 29 | 30 | "listInt.#": "2", 31 | "listInt.0": "21", 32 | "listInt.1": "42", 33 | 34 | "map.%": "2", 35 | "map.foo": "bar", 36 | "map.bar": "baz", 37 | 38 | "set.#": "2", 39 | "set.10": "10", 40 | "set.50": "50", 41 | 42 | "setDeep.#": "2", 43 | "setDeep.10.index": "10", 44 | "setDeep.10.value": "foo", 45 | "setDeep.50.index": "50", 46 | "setDeep.50.value": "bar", 47 | 48 | "mapInt.%": "2", 49 | "mapInt.one": "1", 50 | "mapInt.two": "2", 51 | 52 | "mapIntNestedSchema.%": "2", 53 | "mapIntNestedSchema.one": "1", 54 | "mapIntNestedSchema.two": "2", 55 | 56 | "mapFloat.%": "1", 57 | "mapFloat.oneDotTwo": "1.2", 58 | 59 | "mapBool.%": "2", 60 | "mapBool.True": "true", 61 | "mapBool.False": "false", 62 | }), 63 | } 64 | }) 65 | } 66 | 67 | func TestMapFieldReader_extra(t *testing.T) { 68 | r := &MapFieldReader{ 69 | Schema: map[string]*Schema{ 70 | "mapDel": {Type: TypeMap}, 71 | "mapEmpty": {Type: TypeMap}, 72 | }, 73 | 74 | Map: BasicMapReader(map[string]string{ 75 | "mapDel": "", 76 | 77 | "mapEmpty.%": "0", 78 | }), 79 | } 80 | 81 | cases := map[string]struct { 82 | Addr []string 83 | Out interface{} 84 | OutOk bool 85 | OutComputed bool 86 | OutErr bool 87 | }{ 88 | "mapDel": { 89 | []string{"mapDel"}, 90 | map[string]interface{}{}, 91 | true, 92 | false, 93 | false, 94 | }, 95 | 96 | "mapEmpty": { 97 | []string{"mapEmpty"}, 98 | map[string]interface{}{}, 99 | true, 100 | false, 101 | false, 102 | }, 103 | } 104 | 105 | for name, tc := range cases { 106 | out, err := r.ReadField(tc.Addr) 107 | if err != nil != tc.OutErr { 108 | t.Fatalf("%s: err: %s", name, err) 109 | } 110 | if out.Computed != tc.OutComputed { 111 | t.Fatalf("%s: err: %#v", name, out.Computed) 112 | } 113 | 114 | if s, ok := out.Value.(*Set); ok { 115 | // If it is a set, convert to a list so its more easily checked. 116 | out.Value = s.List() 117 | } 118 | 119 | if !reflect.DeepEqual(out.Value, tc.Out) { 120 | t.Fatalf("%s: out: %#v", name, out.Value) 121 | } 122 | if out.Exists != tc.OutOk { 123 | t.Fatalf("%s: outOk: %#v", name, out.Exists) 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /helper/schema/field_reader_multi.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package schema 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | // MultiLevelFieldReader reads from other field readers, 11 | // merging their results along the way in a specific order. You can specify 12 | // "levels" and name them in order to read only an exact level or up to 13 | // a specific level. 14 | // 15 | // This is useful for saying things such as "read the field from the state 16 | // and config and merge them" or "read the latest value of the field". 17 | type MultiLevelFieldReader struct { 18 | Readers map[string]FieldReader 19 | Levels []string 20 | } 21 | 22 | func (r *MultiLevelFieldReader) ReadField(address []string) (FieldReadResult, error) { 23 | return r.ReadFieldMerge(address, r.Levels[len(r.Levels)-1]) 24 | } 25 | 26 | func (r *MultiLevelFieldReader) ReadFieldExact( 27 | address []string, level string) (FieldReadResult, error) { 28 | reader, ok := r.Readers[level] 29 | if !ok { 30 | return FieldReadResult{}, fmt.Errorf( 31 | "Unknown reader level: %s", level) 32 | } 33 | 34 | result, err := reader.ReadField(address) 35 | if err != nil { 36 | return FieldReadResult{}, fmt.Errorf( 37 | "Error reading level %s: %s", level, err) 38 | } 39 | 40 | return result, nil 41 | } 42 | 43 | func (r *MultiLevelFieldReader) ReadFieldMerge( 44 | address []string, level string) (FieldReadResult, error) { 45 | var result FieldReadResult 46 | for _, l := range r.Levels { 47 | if r, ok := r.Readers[l]; ok { 48 | out, err := r.ReadField(address) 49 | if err != nil { 50 | return FieldReadResult{}, fmt.Errorf( 51 | "Error reading level %s: %s", l, err) 52 | } 53 | 54 | // TODO: computed 55 | if out.Exists { 56 | result = out 57 | } 58 | } 59 | 60 | if l == level { 61 | break 62 | } 63 | } 64 | 65 | return result, nil 66 | } 67 | -------------------------------------------------------------------------------- /helper/schema/field_writer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package schema 5 | 6 | // FieldWriters are responsible for writing fields by address into 7 | // a proper typed representation. ResourceData uses this to write new data 8 | // into existing sources. 9 | type FieldWriter interface { 10 | WriteField([]string, interface{}) error 11 | } 12 | -------------------------------------------------------------------------------- /helper/schema/getsource_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=getSource resource_data_get_source.go"; DO NOT EDIT. 2 | 3 | package schema 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[getSourceState-1] 12 | _ = x[getSourceConfig-2] 13 | _ = x[getSourceDiff-4] 14 | _ = x[getSourceSet-8] 15 | _ = x[getSourceExact-16] 16 | _ = x[getSourceLevelMask-15] 17 | } 18 | 19 | const ( 20 | _getSource_name_0 = "getSourceStategetSourceConfig" 21 | _getSource_name_1 = "getSourceDiff" 22 | _getSource_name_2 = "getSourceSet" 23 | _getSource_name_3 = "getSourceLevelMaskgetSourceExact" 24 | ) 25 | 26 | var ( 27 | _getSource_index_0 = [...]uint8{0, 14, 29} 28 | _getSource_index_3 = [...]uint8{0, 18, 32} 29 | ) 30 | 31 | func (i getSource) String() string { 32 | switch { 33 | case 1 <= i && i <= 2: 34 | i -= 1 35 | return _getSource_name_0[_getSource_index_0[i]:_getSource_index_0[i+1]] 36 | case i == 4: 37 | return _getSource_name_1 38 | case i == 8: 39 | return _getSource_name_2 40 | case 15 <= i && i <= 16: 41 | i -= 15 42 | return _getSource_name_3[_getSource_index_3[i]:_getSource_index_3[i+1]] 43 | default: 44 | return "getSource(" + strconv.FormatInt(int64(i), 10) + ")" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /helper/schema/json.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package schema 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | ) 10 | 11 | func unmarshalJSON(data []byte, v interface{}) error { 12 | dec := json.NewDecoder(bytes.NewReader(data)) 13 | dec.UseNumber() 14 | return dec.Decode(v) 15 | } 16 | -------------------------------------------------------------------------------- /helper/schema/resource_data_get_source.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package schema 5 | 6 | // This code was previously generated with a go:generate directive calling: 7 | // go run golang.org/x/tools/cmd/stringer -type=getSource resource_data_get_source.go 8 | // However, it is now considered frozen and the tooling dependency has been 9 | // removed. The String method can be manually updated if necessary. 10 | 11 | // getSource represents the level we want to get for a value (internally). 12 | // Any source less than or equal to the level will be loaded (whichever 13 | // has a value first). 14 | type getSource byte 15 | 16 | const ( 17 | getSourceState getSource = 1 << iota 18 | getSourceConfig 19 | getSourceDiff 20 | getSourceSet 21 | getSourceExact // Only get from the _exact_ level 22 | getSourceLevelMask getSource = getSourceState | getSourceConfig | getSourceDiff | getSourceSet 23 | ) 24 | -------------------------------------------------------------------------------- /helper/schema/resource_identity_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package schema 5 | 6 | import "testing" 7 | 8 | func TestResourceIdentity_SchemaMap_handles_nil_identity(t *testing.T) { 9 | var ri *ResourceIdentity 10 | if ri.SchemaMap() != nil { 11 | t.Fatal("expected nil schema map") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /helper/schema/testing.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package schema 5 | 6 | import ( 7 | "context" 8 | 9 | testing "github.com/mitchellh/go-testing-interface" 10 | 11 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 12 | ) 13 | 14 | // TestResourceDataRaw creates a ResourceData from a raw configuration map. 15 | func TestResourceDataRaw(t testing.T, schema map[string]*Schema, raw map[string]interface{}) *ResourceData { 16 | t.Helper() 17 | 18 | c := terraform.NewResourceConfigRaw(raw) 19 | 20 | sm := schemaMap(schema) 21 | diff, err := sm.Diff(context.Background(), nil, c, nil, nil, true) 22 | if err != nil { 23 | t.Fatalf("err: %s", err) 24 | } 25 | 26 | result, err := sm.Data(nil, diff) 27 | if err != nil { 28 | t.Fatalf("err: %s", err) 29 | } 30 | 31 | return result 32 | } 33 | 34 | // TestResourceDataWithIdentityRaw creates a ResourceData with an identity from a raw identity map. 35 | func TestResourceDataWithIdentityRaw(t testing.T, schema map[string]*Schema, identitySchema map[string]*Schema, raw map[string]string) *ResourceData { 36 | t.Helper() 37 | 38 | sm := schemaMapWithIdentity{schema, identitySchema} 39 | state := terraform.InstanceState{ 40 | Identity: raw, 41 | } 42 | 43 | result, err := sm.Data(&state, nil) 44 | if err != nil { 45 | t.Fatalf("err: %s", err) 46 | } 47 | 48 | return result 49 | } 50 | -------------------------------------------------------------------------------- /helper/schema/valuetype.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package schema 5 | 6 | // This code was previously generated with a go:generate directive calling: 7 | // go run golang.org/x/tools/cmd/stringer -type=ValueType valuetype.go 8 | // However, it is now considered frozen and the tooling dependency has been 9 | // removed. The String method can be manually updated if necessary. 10 | 11 | // ValueType is an enum of the type that can be represented by a schema. 12 | type ValueType int 13 | 14 | const ( 15 | TypeInvalid ValueType = iota 16 | TypeBool 17 | TypeInt 18 | TypeFloat 19 | TypeString 20 | TypeList 21 | TypeMap 22 | TypeSet 23 | typeObject 24 | ) 25 | 26 | // NOTE: ValueType has more functions defined on it in schema.go. We can't 27 | // put them here because we reference other files. 28 | -------------------------------------------------------------------------------- /helper/schema/valuetype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=ValueType valuetype.go"; DO NOT EDIT. 2 | 3 | package schema 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[TypeInvalid-0] 12 | _ = x[TypeBool-1] 13 | _ = x[TypeInt-2] 14 | _ = x[TypeFloat-3] 15 | _ = x[TypeString-4] 16 | _ = x[TypeList-5] 17 | _ = x[TypeMap-6] 18 | _ = x[TypeSet-7] 19 | _ = x[typeObject-8] 20 | } 21 | 22 | const _ValueType_name = "TypeInvalidTypeBoolTypeIntTypeFloatTypeStringTypeListTypeMapTypeSettypeObject" 23 | 24 | var _ValueType_index = [...]uint8{0, 11, 19, 26, 35, 45, 53, 60, 67, 77} 25 | 26 | func (i ValueType) String() string { 27 | if i < 0 || i >= ValueType(len(_ValueType_index)-1) { 28 | return "ValueType(" + strconv.FormatInt(int64(i), 10) + ")" 29 | } 30 | return _ValueType_name[_ValueType_index[i]:_ValueType_index[i+1]] 31 | } 32 | -------------------------------------------------------------------------------- /helper/structure/expand_json.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package structure 5 | 6 | import "encoding/json" 7 | 8 | func ExpandJsonFromString(jsonString string) (map[string]interface{}, error) { 9 | var result map[string]interface{} 10 | 11 | err := json.Unmarshal([]byte(jsonString), &result) 12 | 13 | return result, err 14 | } 15 | -------------------------------------------------------------------------------- /helper/structure/expand_json_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package structure 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestExpandJson_emptyString(t *testing.T) { 12 | _, err := ExpandJsonFromString("") 13 | if err == nil { 14 | t.Fatal("Expected to throw an error while Expanding JSON") 15 | } 16 | } 17 | 18 | func TestExpandJson_singleItem(t *testing.T) { 19 | input := `{ 20 | "foo": "bar" 21 | }` 22 | expected := make(map[string]interface{}, 1) 23 | expected["foo"] = "bar" 24 | actual, err := ExpandJsonFromString(input) 25 | if err != nil { 26 | t.Fatalf("Expected not to throw an error while Expanding JSON, but got: %s", err) 27 | } 28 | 29 | if !reflect.DeepEqual(actual, expected) { 30 | t.Fatalf("Got:\n\n%+v\n\nExpected:\n\n%+v\n", actual, expected) 31 | } 32 | } 33 | 34 | func TestExpandJson_multipleItems(t *testing.T) { 35 | input := `{ 36 | "foo": "bar", 37 | "hello": "world" 38 | }` 39 | expected := make(map[string]interface{}, 1) 40 | expected["foo"] = "bar" 41 | expected["hello"] = "world" 42 | 43 | actual, err := ExpandJsonFromString(input) 44 | if err != nil { 45 | t.Fatalf("Expected not to throw an error while Expanding JSON, but got: %s", err) 46 | } 47 | 48 | if !reflect.DeepEqual(actual, expected) { 49 | t.Fatalf("Got:\n\n%+v\n\nExpected:\n\n%+v\n", actual, expected) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /helper/structure/flatten_json.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package structure 5 | 6 | import "encoding/json" 7 | 8 | func FlattenJsonToString(input map[string]interface{}) (string, error) { 9 | if len(input) == 0 { 10 | return "", nil 11 | } 12 | 13 | result, err := json.Marshal(input) 14 | if err != nil { 15 | return "", err 16 | } 17 | 18 | return string(result), nil 19 | } 20 | -------------------------------------------------------------------------------- /helper/structure/flatten_json_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package structure 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestFlattenJson_empty(t *testing.T) { 11 | input := make(map[string]interface{}, 0) 12 | expected := "" 13 | actual, err := FlattenJsonToString(input) 14 | if err != nil { 15 | t.Fatalf("Expected not to throw an error while Flattening JSON, but got: %s", err) 16 | } 17 | 18 | if expected != actual { 19 | t.Fatalf("Got: `%+v`. Expected: `%+v`", actual, expected) 20 | } 21 | } 22 | 23 | func TestFlattenJson_singleItem(t *testing.T) { 24 | input := make(map[string]interface{}, 1) 25 | input["foo"] = "bar" 26 | expected := `{"foo":"bar"}` 27 | actual, err := FlattenJsonToString(input) 28 | if err != nil { 29 | t.Fatalf("Expected not to throw an error while Flattening JSON, but got: %s", err) 30 | } 31 | 32 | if expected != actual { 33 | t.Fatalf("Got: `%+v`. Expected: `%+v`", actual, expected) 34 | } 35 | } 36 | 37 | func TestFlattenJson_multipleItems(t *testing.T) { 38 | input := make(map[string]interface{}, 1) 39 | input["foo"] = "bar" 40 | input["bar"] = "foo" 41 | expected := `{"bar":"foo","foo":"bar"}` 42 | actual, err := FlattenJsonToString(input) 43 | if err != nil { 44 | t.Fatalf("Expected not to throw an error while Flattening JSON, but got: %s", err) 45 | } 46 | 47 | if expected != actual { 48 | t.Fatalf("Got: `%+v`. Expected: `%+v`", actual, expected) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /helper/structure/normalize_json.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package structure 5 | 6 | import "encoding/json" 7 | 8 | // Takes a value containing JSON string and passes it through 9 | // the JSON parser to normalize it, returns either a parsing 10 | // error or normalized JSON string. 11 | func NormalizeJsonString(jsonString interface{}) (string, error) { 12 | var j interface{} 13 | 14 | if jsonString == nil || jsonString.(string) == "" { 15 | return "", nil 16 | } 17 | 18 | s := jsonString.(string) 19 | 20 | err := json.Unmarshal([]byte(s), &j) 21 | if err != nil { 22 | return s, err 23 | } 24 | 25 | bytes, _ := json.Marshal(j) 26 | return string(bytes[:]), nil 27 | } 28 | -------------------------------------------------------------------------------- /helper/structure/normalize_json_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package structure 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestNormalizeJsonString_valid(t *testing.T) { 11 | // Well formatted and valid. 12 | validJson := `{ 13 | "abc": { 14 | "def": 123, 15 | "xyz": [ 16 | { 17 | "a": "ホリネズミ" 18 | }, 19 | { 20 | "b": "1\\n2" 21 | } 22 | ] 23 | } 24 | }` 25 | expected := `{"abc":{"def":123,"xyz":[{"a":"ホリネズミ"},{"b":"1\\n2"}]}}` 26 | 27 | actual, err := NormalizeJsonString(validJson) 28 | if err != nil { 29 | t.Fatalf("Expected not to throw an error while parsing JSON, but got: %s", err) 30 | } 31 | 32 | if actual != expected { 33 | t.Fatalf("Got:\n\n%s\n\nExpected:\n\n%s\n", actual, expected) 34 | } 35 | 36 | // Well formatted but not valid, 37 | // missing closing square bracket. 38 | invalidJson := `{ 39 | "abc": { 40 | "def": 123, 41 | "xyz": [ 42 | { 43 | "a": "1" 44 | } 45 | } 46 | } 47 | }` 48 | actual, err = NormalizeJsonString(invalidJson) 49 | if err == nil { 50 | t.Fatalf("Expected to throw an error while parsing JSON, but got: %s", err) 51 | } 52 | 53 | // We expect the invalid JSON to be shown back to us again. 54 | if actual != invalidJson { 55 | t.Fatalf("Got:\n\n%s\n\nExpected:\n\n%s\n", actual, invalidJson) 56 | } 57 | 58 | // Verify that it leaves strings alone 59 | testString := "2016-07-28t04:07:02z\nsomething else" 60 | expected = "2016-07-28t04:07:02z\nsomething else" 61 | actual, err = NormalizeJsonString(testString) 62 | if err == nil { 63 | t.Fatalf("Expected to throw an error while parsing JSON, but got: %s", err) 64 | } 65 | 66 | if actual != expected { 67 | t.Fatalf("Got:\n\n%s\n\nExpected:\n\n%s\n", actual, expected) 68 | } 69 | } 70 | 71 | func TestNormalizeJsonString_invalid(t *testing.T) { 72 | // Well formatted but not valid, 73 | // missing closing square bracket. 74 | invalidJson := `{ 75 | "abc": { 76 | "def": 123, 77 | "xyz": [ 78 | { 79 | "a": "1" 80 | } 81 | } 82 | } 83 | }` 84 | expected := `{"abc":{"def":123,"xyz":[{"a":"ホリネズミ"},{"b":"1\\n2"}]}}` 85 | actual, err := NormalizeJsonString(invalidJson) 86 | if err == nil { 87 | t.Fatalf("Expected to throw an error while parsing JSON, but got: %s", err) 88 | } 89 | 90 | // We expect the invalid JSON to be shown back to us again. 91 | if actual != invalidJson { 92 | t.Fatalf("Got:\n\n%s\n\nExpected:\n\n%s\n", expected, invalidJson) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /helper/structure/suppress_json_diff.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package structure 5 | 6 | import ( 7 | "reflect" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | ) 11 | 12 | func SuppressJsonDiff(k, oldValue, newValue string, d *schema.ResourceData) bool { 13 | oldMap, err := ExpandJsonFromString(oldValue) 14 | if err != nil { 15 | return false 16 | } 17 | 18 | newMap, err := ExpandJsonFromString(newValue) 19 | if err != nil { 20 | return false 21 | } 22 | 23 | return reflect.DeepEqual(oldMap, newMap) 24 | } 25 | -------------------------------------------------------------------------------- /helper/structure/suppress_json_diff_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package structure 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestSuppressJsonDiff(t *testing.T) { 11 | t.Parallel() 12 | 13 | testCases := map[string]struct { 14 | oldValue string 15 | newValue string 16 | expected bool 17 | }{ 18 | "different-structure": { 19 | oldValue: `{ "enabled": true }`, 20 | newValue: `{ "enabled": true, "world": "round" }`, 21 | expected: false, 22 | }, 23 | "different-value": { 24 | oldValue: `{ "enabled": true }`, 25 | newValue: `{ "enabled": false }`, 26 | expected: false, 27 | }, 28 | "same": { 29 | oldValue: `{ "enabled": true }`, 30 | newValue: `{ "enabled": true }`, 31 | expected: true, 32 | }, 33 | "same-whitespace": { 34 | oldValue: `{ 35 | "enabled": true 36 | }`, 37 | newValue: `{ "enabled": true }`, 38 | expected: true, 39 | }, 40 | } 41 | 42 | for name, testCase := range testCases { 43 | t.Run(name, func(t *testing.T) { 44 | t.Parallel() 45 | 46 | actual := SuppressJsonDiff("test", testCase.oldValue, testCase.newValue, nil) 47 | 48 | if actual != testCase.expected { 49 | t.Fatalf("expected %t, got %t", testCase.expected, actual) 50 | } 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /helper/validation/float.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package validation 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | ) 11 | 12 | // FloatBetween returns a SchemaValidateFunc which tests if the provided value 13 | // is of type float64 and is between minVal and maxVal (inclusive). 14 | func FloatBetween(minVal, maxVal float64) schema.SchemaValidateFunc { 15 | return func(i interface{}, k string) (s []string, es []error) { 16 | v, ok := i.(float64) 17 | if !ok { 18 | es = append(es, fmt.Errorf("expected type of %s to be float64", k)) 19 | return 20 | } 21 | 22 | if v < minVal || v > maxVal { 23 | es = append(es, fmt.Errorf("expected %s to be in the range (%f - %f), got %f", k, minVal, maxVal, v)) 24 | return 25 | } 26 | 27 | return 28 | } 29 | } 30 | 31 | // FloatAtLeast returns a SchemaValidateFunc which tests if the provided value 32 | // is of type float and is at least minVal (inclusive) 33 | func FloatAtLeast(minVal float64) schema.SchemaValidateFunc { 34 | return func(i interface{}, k string) (s []string, es []error) { 35 | v, ok := i.(float64) 36 | if !ok { 37 | es = append(es, fmt.Errorf("expected type of %s to be float", k)) 38 | return 39 | } 40 | 41 | if v < minVal { 42 | es = append(es, fmt.Errorf("expected %s to be at least (%f), got %f", k, minVal, v)) 43 | return 44 | } 45 | 46 | return 47 | } 48 | } 49 | 50 | // FloatAtMost returns a SchemaValidateFunc which tests if the provided value 51 | // is of type float and is at most maxVal (inclusive) 52 | func FloatAtMost(maxVal float64) schema.SchemaValidateFunc { 53 | return func(i interface{}, k string) (s []string, es []error) { 54 | v, ok := i.(float64) 55 | if !ok { 56 | es = append(es, fmt.Errorf("expected type of %s to be float", k)) 57 | return 58 | } 59 | 60 | if v > maxVal { 61 | es = append(es, fmt.Errorf("expected %s to be at most (%f), got %f", k, maxVal, v)) 62 | return 63 | } 64 | 65 | return 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /helper/validation/float_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package validation 5 | 6 | import ( 7 | "regexp" 8 | "testing" 9 | 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 11 | ) 12 | 13 | func TestValidateFloatBetween(t *testing.T) { 14 | cases := map[string]struct { 15 | Value interface{} 16 | ValidateFunc schema.SchemaValidateFunc 17 | ExpectValidationErrors bool 18 | }{ 19 | "accept valid value": { 20 | Value: 1.5, 21 | ValidateFunc: FloatBetween(1.0, 2.0), 22 | ExpectValidationErrors: false, 23 | }, 24 | "accept valid value inclusive upper bound": { 25 | Value: 1.0, 26 | ValidateFunc: FloatBetween(0.0, 1.0), 27 | ExpectValidationErrors: false, 28 | }, 29 | "accept valid value inclusive lower bound": { 30 | Value: 0.0, 31 | ValidateFunc: FloatBetween(0.0, 1.0), 32 | ExpectValidationErrors: false, 33 | }, 34 | "reject out of range value": { 35 | Value: -1.0, 36 | ValidateFunc: FloatBetween(0.0, 1.0), 37 | ExpectValidationErrors: true, 38 | }, 39 | "reject incorrectly typed value": { 40 | Value: 1, 41 | ValidateFunc: FloatBetween(0.0, 1.0), 42 | ExpectValidationErrors: true, 43 | }, 44 | } 45 | 46 | for tn, tc := range cases { 47 | _, errors := tc.ValidateFunc(tc.Value, tn) 48 | if len(errors) > 0 && !tc.ExpectValidationErrors { 49 | t.Errorf("%s: unexpected errors %s", tn, errors) 50 | } else if len(errors) == 0 && tc.ExpectValidationErrors { 51 | t.Errorf("%s: expected errors but got none", tn) 52 | } 53 | } 54 | } 55 | 56 | func TestValidateFloatAtLeast(t *testing.T) { 57 | runTestCases(t, []testCase{ 58 | { 59 | val: 2.5, 60 | f: FloatAtLeast(1.5), 61 | }, 62 | { 63 | val: -1.0, 64 | f: FloatAtLeast(-1.5), 65 | }, 66 | { 67 | val: 1.5, 68 | f: FloatAtLeast(2.5), 69 | expectedErr: regexp.MustCompile(`expected [\w]+ to be at least \(2\.5\d*\), got 1\.5\d*`), 70 | }, 71 | { 72 | val: "2.5", 73 | f: FloatAtLeast(1.5), 74 | expectedErr: regexp.MustCompile(`expected type of [\w]+ to be float`), 75 | }, 76 | }) 77 | } 78 | 79 | func TestValidateFloatAtMost(t *testing.T) { 80 | runTestCases(t, []testCase{ 81 | { 82 | val: 2.5, 83 | f: FloatAtMost(3.5), 84 | }, 85 | { 86 | val: -1.0, 87 | f: FloatAtMost(-0.5), 88 | }, 89 | { 90 | val: 2.5, 91 | f: FloatAtMost(1.5), 92 | expectedErr: regexp.MustCompile(`expected [\w]+ to be at most \(1\.5\d*\), got 2\.5\d*`), 93 | }, 94 | { 95 | val: "2.5", 96 | f: FloatAtMost(3.5), 97 | expectedErr: regexp.MustCompile(`expected type of [\w]+ to be float`), 98 | }, 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /helper/validation/list.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package validation 5 | 6 | import "fmt" 7 | 8 | // ListOfUniqueStrings is a ValidateFunc that ensures a list has no 9 | // duplicate items in it. It's useful for when a list is needed over a set 10 | // because order matters, yet the items still need to be unique. 11 | func ListOfUniqueStrings(i interface{}, k string) (warnings []string, errors []error) { 12 | v, ok := i.([]interface{}) 13 | if !ok { 14 | errors = append(errors, fmt.Errorf("expected type of %q to be List", k)) 15 | return warnings, errors 16 | } 17 | 18 | for _, e := range v { 19 | if _, eok := e.(string); !eok { 20 | errors = append(errors, fmt.Errorf("expected %q to only contain string elements, found :%v", k, e)) 21 | return warnings, errors 22 | } 23 | } 24 | 25 | for n1, i1 := range v { 26 | for n2, i2 := range v { 27 | if i1.(string) == i2.(string) && n1 != n2 { 28 | errors = append(errors, fmt.Errorf("expected %q to not have duplicates: found 2 or more of %v", k, i1)) 29 | return warnings, errors 30 | } 31 | } 32 | } 33 | 34 | return warnings, errors 35 | } 36 | -------------------------------------------------------------------------------- /helper/validation/list_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package validation 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestValidationListOfUniqueStrings(t *testing.T) { 11 | cases := map[string]struct { 12 | Value interface{} 13 | Error bool 14 | }{ 15 | "NotList": { 16 | Value: "the list is a lie", 17 | Error: true, 18 | }, 19 | "NotListOfString": { 20 | Value: []interface{}{"seven", 7}, 21 | Error: true, 22 | }, 23 | "NonUniqueStrings": { 24 | Value: []interface{}{"kt", "is", "kt"}, 25 | Error: true, 26 | }, 27 | "UniqueStrings": { 28 | Value: []interface{}{"thanks", "for", "all", "the", "fish"}, 29 | Error: false, 30 | }, 31 | } 32 | 33 | for tn, tc := range cases { 34 | t.Run(tn, func(t *testing.T) { 35 | _, errors := ListOfUniqueStrings(tc.Value, tn) 36 | 37 | if len(errors) > 0 && !tc.Error { 38 | t.Errorf("ListOfUniqueStrings(%s) produced an unexpected error", tc.Value) 39 | } else if len(errors) == 0 && tc.Error { 40 | t.Errorf("ListOfUniqueStrings(%s) did not error", tc.Value) 41 | } 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /helper/validation/path.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package validation 5 | 6 | import ( 7 | "github.com/hashicorp/go-cty/cty" 8 | ) 9 | 10 | // PathMatches compares two Paths for equality. For cty.IndexStep, 11 | // unknown key values are treated as an Any qualifier and will 12 | // match any index step of the same type. 13 | func PathMatches(p cty.Path, other cty.Path) bool { 14 | if len(p) != len(other) { 15 | return false 16 | } 17 | 18 | for i := range p { 19 | pv := p[i] 20 | switch pv := pv.(type) { 21 | case cty.GetAttrStep: 22 | ov, ok := other[i].(cty.GetAttrStep) 23 | if !ok || pv != ov { 24 | return false 25 | } 26 | case cty.IndexStep: 27 | ov, ok := other[i].(cty.IndexStep) 28 | if !ok { 29 | return false 30 | } 31 | 32 | // Sets need special handling since their Type is the entire object 33 | // with attributes. 34 | if pv.Key.Type().IsObjectType() && ov.Key.Type().IsObjectType() { 35 | if !pv.Key.IsKnown() || !ov.Key.IsKnown() { 36 | break 37 | } 38 | } 39 | if !pv.Key.Type().Equals(ov.Key.Type()) { 40 | return false 41 | } 42 | 43 | if pv.Key.IsKnown() && ov.Key.IsKnown() { 44 | if !pv.Key.RawEquals(ov.Key) { 45 | return false 46 | } 47 | } 48 | default: 49 | // Any invalid steps default to evaluating false. 50 | return false 51 | } 52 | } 53 | 54 | return true 55 | } 56 | -------------------------------------------------------------------------------- /helper/validation/testing.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package validation 5 | 6 | import ( 7 | "fmt" 8 | "regexp" 9 | "testing" 10 | 11 | "github.com/hashicorp/go-cty/cty" 12 | 13 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 14 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 15 | ) 16 | 17 | type testCase struct { 18 | val interface{} 19 | f schema.SchemaValidateFunc 20 | expectedErr *regexp.Regexp 21 | } 22 | 23 | func runTestCases(t *testing.T, cases []testCase) { 24 | t.Helper() 25 | 26 | for i, tc := range cases { 27 | t.Run(fmt.Sprintf("TestCase_%d", i), func(t *testing.T) { 28 | _, errs := tc.f(tc.val, "test_property") 29 | 30 | if len(errs) == 0 && tc.expectedErr == nil { 31 | return 32 | } 33 | 34 | if len(errs) != 0 && tc.expectedErr == nil { 35 | t.Fatalf("expected test case %d to produce no errors, got %v", i, errs) 36 | } 37 | 38 | if !matchAnyError(errs, tc.expectedErr) { 39 | t.Fatalf("expected test case %d to produce error matching \"%s\", got %v", i, tc.expectedErr, errs) 40 | } 41 | }) 42 | } 43 | } 44 | 45 | type diagTestCase struct { 46 | val interface{} 47 | f schema.SchemaValidateDiagFunc 48 | expectedDiagSummary *regexp.Regexp 49 | } 50 | 51 | func runDiagTestCases(t *testing.T, cases []diagTestCase) { 52 | t.Helper() 53 | 54 | for i, tc := range cases { 55 | t.Run(fmt.Sprintf("TestCase_%d", i), func(t *testing.T) { 56 | diags := tc.f(tc.val, cty.GetAttrPath("test_property")) 57 | 58 | if len(diags) == 0 && tc.expectedDiagSummary == nil { 59 | return 60 | } 61 | 62 | if len(diags) != 0 && tc.expectedDiagSummary == nil { 63 | t.Fatalf("expected test case %d to produce no diagnostics, got %v", i, diags) 64 | } 65 | 66 | if !matchAnyDiagSummary(diags, tc.expectedDiagSummary) { 67 | t.Fatalf("expected test case %d to produce diagnostic summary matching \"%s\", got %v", i, tc.expectedDiagSummary, diags) 68 | } 69 | }) 70 | } 71 | } 72 | 73 | func matchAnyError(errs []error, r *regexp.Regexp) bool { 74 | // err must match one provided 75 | for _, err := range errs { 76 | if r.MatchString(err.Error()) { 77 | return true 78 | } 79 | } 80 | return false 81 | } 82 | 83 | func matchAnyDiagSummary(ds diag.Diagnostics, r *regexp.Regexp) bool { 84 | for _, d := range ds { 85 | if r.MatchString(d.Summary) { 86 | return true 87 | } 88 | } 89 | return false 90 | } 91 | -------------------------------------------------------------------------------- /helper/validation/time.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package validation 5 | 6 | import ( 7 | "fmt" 8 | "time" 9 | 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 11 | ) 12 | 13 | // IsDayOfTheWeek is a SchemaValidateFunc which tests if the provided value is of type string and a valid english day of the week 14 | func IsDayOfTheWeek(ignoreCase bool) schema.SchemaValidateFunc { 15 | return StringInSlice([]string{ 16 | "Monday", 17 | "Tuesday", 18 | "Wednesday", 19 | "Thursday", 20 | "Friday", 21 | "Saturday", 22 | "Sunday", 23 | }, ignoreCase) 24 | } 25 | 26 | // IsMonth is a SchemaValidateFunc which tests if the provided value is of type string and a valid english month 27 | func IsMonth(ignoreCase bool) schema.SchemaValidateFunc { 28 | return StringInSlice([]string{ 29 | "January", 30 | "February", 31 | "March", 32 | "April", 33 | "May", 34 | "June", 35 | "July", 36 | "August", 37 | "September", 38 | "October", 39 | "November", 40 | "December", 41 | }, ignoreCase) 42 | } 43 | 44 | // IsRFC3339Time is a SchemaValidateFunc which tests if the provided value is of type string and a valid RFC33349Time 45 | func IsRFC3339Time(i interface{}, k string) (warnings []string, errors []error) { 46 | v, ok := i.(string) 47 | if !ok { 48 | errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) 49 | return warnings, errors 50 | } 51 | 52 | if _, err := time.Parse(time.RFC3339, v); err != nil { 53 | errors = append(errors, fmt.Errorf("expected %q to be a valid RFC3339 date, got %q: %+v", k, i, err)) 54 | } 55 | 56 | return warnings, errors 57 | } 58 | -------------------------------------------------------------------------------- /helper/validation/time_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package validation 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestValidationIsRFC3339Time(t *testing.T) { 11 | cases := map[string]struct { 12 | Value interface{} 13 | Error bool 14 | }{ 15 | "NotString": { 16 | Value: 7, 17 | Error: true, 18 | }, 19 | "ValidDate": { 20 | Value: "2018-03-01T00:00:00Z", 21 | Error: false, 22 | }, 23 | "ValidDateTime": { 24 | Value: "2018-03-01T00:00:00-05:00", 25 | Error: false, 26 | }, 27 | "ValidDateTime2": { 28 | Value: "2018-03-01T00:00:00+05:00", 29 | Error: false, 30 | }, 31 | "InvalidDateWithSlashes": { 32 | Value: "03/01/2018", 33 | Error: true, 34 | }, 35 | "InvalidDateWithDashes": { 36 | Value: "03-01-2018", 37 | Error: true, 38 | }, 39 | "InvalidDateWithDashes2": { 40 | Value: "2018-03-01", 41 | Error: true, 42 | }, 43 | "InvalidDateWithT": { 44 | Value: "2018-03-01T", 45 | Error: true, 46 | }, 47 | "DateTimeWithoutZone": { 48 | Value: "2018-03-01T00:00:00", 49 | Error: true, 50 | }, 51 | "DateTimeWithZZone": { 52 | Value: "2018-03-01T00:00:00Z05:00", 53 | Error: true, 54 | }, 55 | "DateTimeWithZZoneNeg": { 56 | Value: "2018-03-01T00:00:00Z-05:00", 57 | Error: true, 58 | }, 59 | } 60 | 61 | for tn, tc := range cases { 62 | t.Run(tn, func(t *testing.T) { 63 | _, errors := IsRFC3339Time(tc.Value, tn) 64 | 65 | if len(errors) > 0 && !tc.Error { 66 | t.Errorf("IsRFC3339Time(%s) produced an unexpected error", tc.Value) 67 | } else if len(errors) == 0 && tc.Error { 68 | t.Errorf("IsRFC3339Time(%s) did not error", tc.Value) 69 | } 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /helper/validation/uuid.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package validation 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/hashicorp/go-uuid" 10 | ) 11 | 12 | // IsUUID is a ValidateFunc that ensures a string can be parsed as UUID 13 | func IsUUID(i interface{}, k string) (warnings []string, errors []error) { 14 | v, ok := i.(string) 15 | if !ok { 16 | errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) 17 | return 18 | } 19 | 20 | if _, err := uuid.ParseUUID(v); err != nil { 21 | errors = append(errors, fmt.Errorf("expected %q to be a valid UUID, got %v", k, v)) 22 | } 23 | 24 | return warnings, errors 25 | } 26 | -------------------------------------------------------------------------------- /helper/validation/uuid_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package validation 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestValidationIsUUID(t *testing.T) { 11 | cases := map[string]struct { 12 | Value interface{} 13 | Error bool 14 | }{ 15 | "NotString": { 16 | Value: 7, 17 | Error: true, 18 | }, 19 | "Empty": { 20 | Value: "", 21 | Error: true, 22 | }, 23 | "InvalidUuid": { 24 | Value: "00000000-0000-123-0000-000000000000", 25 | Error: true, 26 | }, 27 | "ValidUuidWithoutDashes": { 28 | Value: "12345678123412341234123456789012", 29 | Error: true, 30 | }, 31 | "ValidUuid": { 32 | Value: "00000000-0000-0000-0000-000000000000", 33 | Error: false, 34 | }, 35 | } 36 | 37 | for tn, tc := range cases { 38 | t.Run(tn, func(t *testing.T) { 39 | _, errors := IsUUID(tc.Value, tn) 40 | 41 | if len(errors) > 0 && !tc.Error { 42 | t.Errorf("IsUUID(%s) produced an unexpected error", tc.Value) 43 | } else if len(errors) == 0 && tc.Error { 44 | t.Errorf("IsUUID(%s) did not error", tc.Value) 45 | } 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /helper/validation/web.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package validation 5 | 6 | import ( 7 | "fmt" 8 | "net/url" 9 | "strings" 10 | 11 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 12 | ) 13 | 14 | // IsURLWithHTTPS is a SchemaValidateFunc which tests if the provided value is of type string and a valid HTTPS URL 15 | func IsURLWithHTTPS(i interface{}, k string) (_ []string, errors []error) { 16 | return IsURLWithScheme([]string{"https"})(i, k) 17 | } 18 | 19 | // IsURLWithHTTPorHTTPS is a SchemaValidateFunc which tests if the provided value is of type string and a valid HTTP or HTTPS URL 20 | func IsURLWithHTTPorHTTPS(i interface{}, k string) (_ []string, errors []error) { 21 | return IsURLWithScheme([]string{"http", "https"})(i, k) 22 | } 23 | 24 | // IsURLWithScheme is a SchemaValidateFunc which tests if the provided value is of type string and a valid URL with the provided schemas 25 | func IsURLWithScheme(validSchemes []string) schema.SchemaValidateFunc { 26 | return func(i interface{}, k string) (_ []string, errors []error) { 27 | v, ok := i.(string) 28 | if !ok { 29 | errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) 30 | return 31 | } 32 | 33 | if v == "" { 34 | errors = append(errors, fmt.Errorf("expected %q url to not be empty, got %v", k, i)) 35 | return 36 | } 37 | 38 | u, err := url.Parse(v) 39 | if err != nil { 40 | errors = append(errors, fmt.Errorf("expected %q to be a valid url, got %v: %+v", k, v, err)) 41 | return 42 | } 43 | 44 | if u.Host == "" { 45 | errors = append(errors, fmt.Errorf("expected %q to have a host, got %v", k, v)) 46 | return 47 | } 48 | 49 | for _, s := range validSchemes { 50 | if u.Scheme == s { 51 | return //last check so just return 52 | } 53 | } 54 | 55 | errors = append(errors, fmt.Errorf("expected %q to have a url with schema of: %q, got %v", k, strings.Join(validSchemes, ","), v)) 56 | return 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /helper/validation/web_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package validation 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestValidationIsURLWithHTTPS(t *testing.T) { 11 | cases := map[string]struct { 12 | Value interface{} 13 | Error bool 14 | }{ 15 | "NotString": { 16 | Value: 7, 17 | Error: true, 18 | }, 19 | "Empty": { 20 | Value: "", 21 | Error: true, 22 | }, 23 | "NotUrl": { 24 | Value: "this is not a url", 25 | Error: true, 26 | }, 27 | "BareUrl": { 28 | Value: "www.example.com", 29 | Error: true, 30 | }, 31 | "FtpUrl": { 32 | Value: "ftp://www.example.com", 33 | Error: true, 34 | }, 35 | "HttpUrl": { 36 | Value: "http://www.example.com", 37 | Error: true, 38 | }, 39 | "HttpsUrl": { 40 | Value: "https://www.example.com", 41 | Error: false, 42 | }, 43 | } 44 | 45 | for tn, tc := range cases { 46 | t.Run(tn, func(t *testing.T) { 47 | _, errors := IsURLWithHTTPS(tc.Value, tn) 48 | 49 | if len(errors) > 0 && !tc.Error { 50 | t.Errorf("IsURLWithHTTPS(%s) produced an unexpected error", tc.Value) 51 | } else if len(errors) == 0 && tc.Error { 52 | t.Errorf("IsURLWithHTTPS(%s) did not error", tc.Value) 53 | } 54 | }) 55 | } 56 | } 57 | 58 | func TestValidationIsURLWithHTTPorHTTPS(t *testing.T) { 59 | cases := map[string]struct { 60 | Value interface{} 61 | Error bool 62 | }{ 63 | "NotString": { 64 | Value: 7, 65 | Error: true, 66 | }, 67 | "Empty": { 68 | Value: "", 69 | Error: true, 70 | }, 71 | "NotUrl": { 72 | Value: "this is not a url", 73 | Error: true, 74 | }, 75 | "BareUrl": { 76 | Value: "www.example.com", 77 | Error: true, 78 | }, 79 | "FtpUrl": { 80 | Value: "ftp://www.example.com", 81 | Error: true, 82 | }, 83 | "HttpUrl": { 84 | Value: "http://www.example.com", 85 | Error: false, 86 | }, 87 | "HttpsUrl": { 88 | Value: "https://www.example.com", 89 | Error: false, 90 | }, 91 | } 92 | 93 | for tn, tc := range cases { 94 | t.Run(tn, func(t *testing.T) { 95 | _, errors := IsURLWithHTTPorHTTPS(tc.Value, tn) 96 | 97 | if len(errors) > 0 && !tc.Error { 98 | t.Errorf("IsURLWithHTTPorHTTPS(%s) produced an unexpected error", tc.Value) 99 | } else if len(errors) == 0 && tc.Error { 100 | t.Errorf("IsURLWithHTTPorHTTPS(%s) did not error", tc.Value) 101 | } 102 | }) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /internal/addrs/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package addrs contains types that represent "addresses", which are 5 | // references to specific objects within a Terraform configuration or 6 | // state. 7 | // 8 | // All addresses have string representations based on HCL traversal syntax 9 | // which should be used in the user-interface, and also in-memory 10 | // representations that can be used internally. 11 | // 12 | // For object types that exist within Terraform modules a pair of types is 13 | // used. The "local" part of the address is represented by a type, and then 14 | // an absolute path to that object in the context of its module is represented 15 | // by a type of the same name with an "Abs" prefix added, for "absolute". 16 | // 17 | // All types within this package should be treated as immutable, even if this 18 | // is not enforced by the Go compiler. It is always an implementation error 19 | // to modify an address object in-place after it is initially constructed. 20 | package addrs 21 | -------------------------------------------------------------------------------- /internal/addrs/instance_key.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package addrs 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | // instanceKey represents the key of an instance within an object that 11 | // contains multiple instances due to using "count" or "for_each" arguments 12 | // in configuration. 13 | // 14 | // intKey and stringKey are the two implementations of this type. No other 15 | // implementations are allowed. The single instance of an object that _isn't_ 16 | // using "count" or "for_each" is represented by NoKey, which is a nil 17 | // InstanceKey. 18 | type instanceKey interface { 19 | instanceKeySigil() 20 | String() string 21 | } 22 | 23 | // NoKey represents the absence of an instanceKey, for the single instance 24 | // of a configuration object that does not use "count" or "for_each" at all. 25 | var NoKey instanceKey 26 | 27 | // intKey is the InstanceKey representation representing integer indices, as 28 | // used when the "count" argument is specified or if for_each is used with 29 | // a sequence type. 30 | type intKey int 31 | 32 | func (k intKey) instanceKeySigil() { 33 | } 34 | 35 | func (k intKey) String() string { 36 | return fmt.Sprintf("[%d]", int(k)) 37 | } 38 | 39 | // stringKey is the InstanceKey representation representing string indices, as 40 | // used when the "for_each" argument is specified with a map or object type. 41 | type stringKey string 42 | 43 | func (k stringKey) instanceKeySigil() { 44 | } 45 | 46 | func (k stringKey) String() string { 47 | // FIXME: This isn't _quite_ right because Go's quoted string syntax is 48 | // slightly different than HCL's, but we'll accept it for now. 49 | return fmt.Sprintf("[%q]", string(k)) 50 | } 51 | -------------------------------------------------------------------------------- /internal/addrs/module.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package addrs 5 | 6 | // Module is an address for a module call within configuration. This is 7 | // the static counterpart of ModuleInstance, representing a traversal through 8 | // the static module call tree in configuration and does not take into account 9 | // the potentially-multiple instances of a module that might be created by 10 | // "count" and "for_each" arguments within those calls. 11 | // 12 | // This type should be used only in very specialized cases when working with 13 | // the static module call tree. Type ModuleInstance is appropriate in more cases. 14 | // 15 | // Although Module is a slice, it should be treated as immutable after creation. 16 | type Module []string 17 | -------------------------------------------------------------------------------- /internal/configs/configschema/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package configschema contains types for describing the expected structure 5 | // of a configuration block whose shape is not known until runtime. 6 | // 7 | // For example, this is used to describe the expected contents of a resource 8 | // configuration block, which is defined by the corresponding provider plugin 9 | // and thus not compiled into Terraform core. 10 | // 11 | // A configschema primarily describes the shape of configuration, but it is 12 | // also suitable for use with other structures derived from the configuration, 13 | // such as the cached state of a resource or a resource diff. 14 | // 15 | // This package should not be confused with the package helper/schema, which 16 | // is the higher-level helper library used to implement providers themselves. 17 | package configschema 18 | -------------------------------------------------------------------------------- /internal/configs/configschema/empty_value.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package configschema 5 | 6 | import ( 7 | "github.com/hashicorp/go-cty/cty" 8 | ) 9 | 10 | // EmptyValue returns the "empty value" for the receiving block, which for 11 | // a block type is a non-null object where all of the attribute values are 12 | // the empty values of the block's attributes and nested block types. 13 | // 14 | // In other words, it returns the value that would be returned if an empty 15 | // block were decoded against the receiving schema, assuming that no required 16 | // attribute or block constraints were honored. 17 | func (b *Block) EmptyValue() cty.Value { 18 | vals := make(map[string]cty.Value) 19 | for name, attrS := range b.Attributes { 20 | vals[name] = attrS.EmptyValue() 21 | } 22 | for name, blockS := range b.BlockTypes { 23 | vals[name] = blockS.EmptyValue() 24 | } 25 | return cty.ObjectVal(vals) 26 | } 27 | 28 | // EmptyValue returns the "empty value" for the receiving attribute, which is 29 | // the value that would be returned if there were no definition of the attribute 30 | // at all, ignoring any required constraint. 31 | func (a *Attribute) EmptyValue() cty.Value { 32 | return cty.NullVal(a.Type) 33 | } 34 | 35 | // EmptyValue returns the "empty value" for when there are zero nested blocks 36 | // present of the receiving type. 37 | func (b *NestedBlock) EmptyValue() cty.Value { 38 | switch b.Nesting { 39 | case NestingSingle: 40 | return cty.NullVal(b.Block.ImpliedType()) 41 | case NestingGroup: 42 | return b.Block.EmptyValue() 43 | case NestingList: 44 | if ty := b.Block.ImpliedType(); ty.HasDynamicTypes() { 45 | return cty.EmptyTupleVal 46 | } else { 47 | return cty.ListValEmpty(ty) 48 | } 49 | case NestingMap: 50 | if ty := b.Block.ImpliedType(); ty.HasDynamicTypes() { 51 | return cty.EmptyObjectVal 52 | } else { 53 | return cty.MapValEmpty(ty) 54 | } 55 | case NestingSet: 56 | return cty.SetValEmpty(b.Block.ImpliedType()) 57 | default: 58 | // Should never get here because the above is intended to be exhaustive, 59 | // but we'll be robust and return a result nonetheless. 60 | return cty.NullVal(cty.DynamicPseudoType) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /internal/configs/configschema/implied_type.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package configschema 5 | 6 | import ( 7 | "github.com/hashicorp/go-cty/cty" 8 | ) 9 | 10 | // ImpliedType returns the cty.Type that would result from decoding a 11 | // configuration block using the receiving block schema. 12 | // 13 | // ImpliedType always returns a result, even if the given schema is 14 | // inconsistent. 15 | func (b *Block) ImpliedType() cty.Type { 16 | if b == nil { 17 | return cty.EmptyObject 18 | } 19 | 20 | atys := make(map[string]cty.Type) 21 | 22 | for name, attrS := range b.Attributes { 23 | atys[name] = attrS.Type 24 | } 25 | 26 | for name, blockS := range b.BlockTypes { 27 | if _, exists := atys[name]; exists { 28 | panic("invalid schema, blocks and attributes cannot have the same name") 29 | } 30 | 31 | childType := blockS.Block.ImpliedType() 32 | 33 | switch blockS.Nesting { 34 | case NestingSingle, NestingGroup: 35 | atys[name] = childType 36 | case NestingList: 37 | // We prefer to use a list where possible, since it makes our 38 | // implied type more complete, but if there are any 39 | // dynamically-typed attributes inside we must use a tuple 40 | // instead, which means our type _constraint_ must be 41 | // cty.DynamicPseudoType to allow the tuple type to be decided 42 | // separately for each value. 43 | if childType.HasDynamicTypes() { 44 | atys[name] = cty.DynamicPseudoType 45 | } else { 46 | atys[name] = cty.List(childType) 47 | } 48 | case NestingSet: 49 | if childType.HasDynamicTypes() { 50 | panic("can't use cty.DynamicPseudoType inside a block type with NestingSet") 51 | } 52 | atys[name] = cty.Set(childType) 53 | case NestingMap: 54 | // We prefer to use a map where possible, since it makes our 55 | // implied type more complete, but if there are any 56 | // dynamically-typed attributes inside we must use an object 57 | // instead, which means our type _constraint_ must be 58 | // cty.DynamicPseudoType to allow the tuple type to be decided 59 | // separately for each value. 60 | if childType.HasDynamicTypes() { 61 | atys[name] = cty.DynamicPseudoType 62 | } else { 63 | atys[name] = cty.Map(childType) 64 | } 65 | default: 66 | panic("invalid nesting type") 67 | } 68 | } 69 | 70 | return cty.Object(atys) 71 | } 72 | -------------------------------------------------------------------------------- /internal/configs/configschema/implied_type_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package configschema 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/hashicorp/go-cty/cty" 10 | ) 11 | 12 | func TestBlockImpliedType(t *testing.T) { 13 | tests := map[string]struct { 14 | Schema *Block 15 | Want cty.Type 16 | }{ 17 | "nil": { 18 | nil, 19 | cty.EmptyObject, 20 | }, 21 | "empty": { 22 | &Block{}, 23 | cty.EmptyObject, 24 | }, 25 | "attributes": { 26 | &Block{ 27 | Attributes: map[string]*Attribute{ 28 | "optional": { 29 | Type: cty.String, 30 | Optional: true, 31 | }, 32 | "required": { 33 | Type: cty.Number, 34 | Required: true, 35 | }, 36 | "computed": { 37 | Type: cty.List(cty.Bool), 38 | Computed: true, 39 | }, 40 | "optional_computed": { 41 | Type: cty.Map(cty.Bool), 42 | Optional: true, 43 | }, 44 | }, 45 | }, 46 | cty.Object(map[string]cty.Type{ 47 | "optional": cty.String, 48 | "required": cty.Number, 49 | "computed": cty.List(cty.Bool), 50 | "optional_computed": cty.Map(cty.Bool), 51 | }), 52 | }, 53 | "blocks": { 54 | &Block{ 55 | BlockTypes: map[string]*NestedBlock{ 56 | "single": { 57 | Nesting: NestingSingle, 58 | Block: Block{ 59 | Attributes: map[string]*Attribute{ 60 | "foo": { 61 | Type: cty.DynamicPseudoType, 62 | Required: true, 63 | }, 64 | }, 65 | }, 66 | }, 67 | "list": { 68 | Nesting: NestingList, 69 | }, 70 | "set": { 71 | Nesting: NestingSet, 72 | }, 73 | "map": { 74 | Nesting: NestingMap, 75 | }, 76 | }, 77 | }, 78 | cty.Object(map[string]cty.Type{ 79 | "single": cty.Object(map[string]cty.Type{ 80 | "foo": cty.DynamicPseudoType, 81 | }), 82 | "list": cty.List(cty.EmptyObject), 83 | "set": cty.Set(cty.EmptyObject), 84 | "map": cty.Map(cty.EmptyObject), 85 | }), 86 | }, 87 | "deep block nesting": { 88 | &Block{ 89 | BlockTypes: map[string]*NestedBlock{ 90 | "single": { 91 | Nesting: NestingSingle, 92 | Block: Block{ 93 | BlockTypes: map[string]*NestedBlock{ 94 | "list": { 95 | Nesting: NestingList, 96 | Block: Block{ 97 | BlockTypes: map[string]*NestedBlock{ 98 | "set": { 99 | Nesting: NestingSet, 100 | }, 101 | }, 102 | }, 103 | }, 104 | }, 105 | }, 106 | }, 107 | }, 108 | }, 109 | cty.Object(map[string]cty.Type{ 110 | "single": cty.Object(map[string]cty.Type{ 111 | "list": cty.List(cty.Object(map[string]cty.Type{ 112 | "set": cty.Set(cty.EmptyObject), 113 | })), 114 | }), 115 | }), 116 | }, 117 | } 118 | 119 | for name, test := range tests { 120 | t.Run(name, func(t *testing.T) { 121 | got := test.Schema.ImpliedType() 122 | if !got.Equals(test.Want) { 123 | t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) 124 | } 125 | }) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /internal/configs/configschema/nestingmode_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=NestingMode"; DO NOT EDIT. 2 | 3 | package configschema 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[nestingModeInvalid-0] 12 | _ = x[NestingSingle-1] 13 | _ = x[NestingGroup-2] 14 | _ = x[NestingList-3] 15 | _ = x[NestingSet-4] 16 | _ = x[NestingMap-5] 17 | } 18 | 19 | const _NestingMode_name = "nestingModeInvalidNestingSingleNestingGroupNestingListNestingSetNestingMap" 20 | 21 | var _NestingMode_index = [...]uint8{0, 18, 31, 43, 54, 64, 74} 22 | 23 | func (i NestingMode) String() string { 24 | if i < 0 || i >= NestingMode(len(_NestingMode_index)-1) { 25 | return "NestingMode(" + strconv.FormatInt(int64(i), 10) + ")" 26 | } 27 | return _NestingMode_name[_NestingMode_index[i]:_NestingMode_index[i+1]] 28 | } 29 | -------------------------------------------------------------------------------- /internal/diagutils/diagutils.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package diagutils 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 11 | ) 12 | 13 | type ErrorDiags diag.Diagnostics 14 | 15 | func (diags ErrorDiags) Errors() []error { 16 | var es []error 17 | for i := range diags { 18 | if diags[i].Severity == diag.Error { 19 | s := fmt.Sprintf("Error: %s", diags[i].Summary) 20 | if diags[i].Detail != "" { 21 | s = fmt.Sprintf("%s: %s", s, diags[i].Detail) 22 | } 23 | es = append(es, errors.New(s)) 24 | } 25 | } 26 | return es 27 | } 28 | 29 | func (diags ErrorDiags) Error() string { 30 | return errors.Join(diags.Errors()...).Error() 31 | } 32 | 33 | type WarningDiags diag.Diagnostics 34 | 35 | func (diags WarningDiags) Warnings() []string { 36 | var ws []string 37 | for i := range diags { 38 | if diags[i].Severity == diag.Warning { 39 | s := fmt.Sprintf("Warning: %s", diags[i].Summary) 40 | if diags[i].Detail != "" { 41 | s = fmt.Sprintf("%s: %s", s, diags[i].Detail) 42 | } 43 | ws = append(ws, s) 44 | } 45 | } 46 | return ws 47 | } 48 | -------------------------------------------------------------------------------- /internal/helper/hashcode/hashcode.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package hashcode 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "hash/crc32" 10 | ) 11 | 12 | // String hashes a string to a unique hashcode. 13 | // 14 | // crc32 returns a uint32, but for our use we need 15 | // and non negative integer. Here we cast to an integer 16 | // and invert it if the result is negative. 17 | func String(s string) int { 18 | v := int(crc32.ChecksumIEEE([]byte(s))) 19 | if v >= 0 { 20 | return v 21 | } 22 | if -v >= 0 { 23 | return -v 24 | } 25 | // v == MinInt 26 | return 0 27 | } 28 | 29 | // Strings hashes a list of strings to a unique hashcode. 30 | func Strings(strings []string) string { 31 | var buf bytes.Buffer 32 | 33 | for _, s := range strings { 34 | buf.WriteString(fmt.Sprintf("%s-", s)) 35 | } 36 | 37 | return fmt.Sprintf("%d", String(buf.String())) 38 | } 39 | -------------------------------------------------------------------------------- /internal/helper/hashcode/hashcode_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package hashcode 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestString(t *testing.T) { 11 | v := "hello, world" 12 | expected := String(v) 13 | for i := 0; i < 100; i++ { 14 | actual := String(v) 15 | if actual != expected { 16 | t.Fatalf("bad: %#v\n\t%#v", actual, expected) 17 | } 18 | } 19 | } 20 | 21 | func TestStrings(t *testing.T) { 22 | v := []string{"hello", ",", "world"} 23 | expected := Strings(v) 24 | for i := 0; i < 100; i++ { 25 | actual := Strings(v) 26 | if actual != expected { 27 | t.Fatalf("bad: %#v\n\t%#v", actual, expected) 28 | } 29 | } 30 | } 31 | 32 | func TestString_positiveIndex(t *testing.T) { 33 | // "2338615298" hashes to uint32(2147483648) which is math.MinInt32 34 | ips := []string{"192.168.1.3", "192.168.1.5", "2338615298"} 35 | for _, ip := range ips { 36 | if index := String(ip); index < 0 { 37 | t.Fatalf("Bad Index %#v for ip %s", index, ip) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/logging/context.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package logging 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/terraform-plugin-log/tfsdklog" 10 | helperlogging "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" 11 | testing "github.com/mitchellh/go-testing-interface" 12 | ) 13 | 14 | // InitContext creates SDK logger contexts when the provider is running in 15 | // "production" (not under acceptance testing). The incoming context will 16 | // already have the root SDK logger and root provider logger setup from 17 | // terraform-plugin-go tf5server RPC handlers. 18 | func InitContext(ctx context.Context) context.Context { 19 | ctx = tfsdklog.NewSubsystem(ctx, SubsystemHelperSchema, 20 | // All calls are through the HelperSchema* helper functions 21 | tfsdklog.WithAdditionalLocationOffset(1), 22 | tfsdklog.WithLevelFromEnv(EnvTfLogSdkHelperSchema), 23 | // Propagate tf_req_id, tf_rpc, etc. fields 24 | tfsdklog.WithRootFields(), 25 | ) 26 | 27 | return ctx 28 | } 29 | 30 | // InitTestContext registers the terraform-plugin-log/tfsdklog test sink, 31 | // configures the standard library log package, and creates SDK logger 32 | // contexts. The incoming context is expected to be devoid of logging setup. 33 | // 34 | // The standard library log package handling is important as provider code 35 | // under test may be using that package or another logging library outside of 36 | // terraform-plugin-log. 37 | func InitTestContext(ctx context.Context, t testing.T) context.Context { 38 | helperlogging.SetOutput(t) 39 | 40 | ctx = tfsdklog.RegisterTestSink(ctx, t) 41 | ctx = tfsdklog.NewRootSDKLogger(ctx, tfsdklog.WithLevelFromEnv(EnvTfLogSdk)) 42 | ctx = tfsdklog.NewSubsystem(ctx, SubsystemHelperResource, 43 | // All calls are through the HelperResource* helper functions 44 | tfsdklog.WithAdditionalLocationOffset(1), 45 | tfsdklog.WithLevelFromEnv(EnvTfLogSdkHelperResource), 46 | ) 47 | ctx = TestNameContext(ctx, t.Name()) 48 | 49 | return ctx 50 | } 51 | 52 | // TestNameContext adds the current test name to loggers. 53 | func TestNameContext(ctx context.Context, testName string) context.Context { 54 | ctx = tfsdklog.SubsystemSetField(ctx, SubsystemHelperResource, KeyTestName, testName) 55 | 56 | return ctx 57 | } 58 | 59 | // TestStepNumberContext adds the current test step number to loggers. 60 | func TestStepNumberContext(ctx context.Context, stepNumber int) context.Context { 61 | ctx = tfsdklog.SubsystemSetField(ctx, SubsystemHelperResource, KeyTestStepNumber, stepNumber) 62 | 63 | return ctx 64 | } 65 | 66 | // TestTerraformPathContext adds the current test Terraform CLI path to loggers. 67 | func TestTerraformPathContext(ctx context.Context, terraformPath string) context.Context { 68 | ctx = tfsdklog.SubsystemSetField(ctx, SubsystemHelperResource, KeyTestTerraformPath, terraformPath) 69 | 70 | return ctx 71 | } 72 | 73 | // TestWorkingDirectoryContext adds the current test working directory to loggers. 74 | func TestWorkingDirectoryContext(ctx context.Context, workingDirectory string) context.Context { 75 | ctx = tfsdklog.SubsystemSetField(ctx, SubsystemHelperResource, KeyTestWorkingDirectory, workingDirectory) 76 | 77 | return ctx 78 | } 79 | -------------------------------------------------------------------------------- /internal/logging/environment_variables.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package logging 5 | 6 | // Environment variables. 7 | const ( 8 | // EnvTfLogSdk is an environment variable that sets the logging level of 9 | // the root SDK logger, while the provider is under test. In "production" 10 | // usage, this environment variable is handled by terraform-plugin-go. 11 | // 12 | // Terraform CLI's logging must be explicitly turned on before this 13 | // environment variable can be used to reduce the SDK logging levels. It 14 | // cannot be used to show only SDK logging unless all other logging levels 15 | // are turned off. 16 | EnvTfLogSdk = "TF_LOG_SDK" 17 | 18 | // EnvTfLogSdkHelperResource is an environment variable that sets the logging 19 | // level of SDK helper/resource loggers. Infers root SDK logging level, if 20 | // unset. 21 | EnvTfLogSdkHelperResource = "TF_LOG_SDK_HELPER_RESOURCE" 22 | 23 | // EnvTfLogSdkHelperSchema is an environment variable that sets the logging 24 | // level of SDK helper/schema loggers. Infers root SDK logging level, if 25 | // unset. 26 | EnvTfLogSdkHelperSchema = "TF_LOG_SDK_HELPER_SCHEMA" 27 | ) 28 | -------------------------------------------------------------------------------- /internal/logging/helper_resource.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package logging 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/terraform-plugin-log/tfsdklog" 10 | ) 11 | 12 | const ( 13 | // SubsystemHelperResource is the tfsdklog subsystem name for helper/resource. 14 | SubsystemHelperResource = "helper_resource" 15 | ) 16 | 17 | // HelperResourceTrace emits a helper/resource subsystem log at TRACE level. 18 | func HelperResourceTrace(ctx context.Context, msg string, additionalFields ...map[string]interface{}) { 19 | tfsdklog.SubsystemTrace(ctx, SubsystemHelperResource, msg, additionalFields...) 20 | } 21 | 22 | // HelperResourceDebug emits a helper/resource subsystem log at DEBUG level. 23 | func HelperResourceDebug(ctx context.Context, msg string, additionalFields ...map[string]interface{}) { 24 | tfsdklog.SubsystemDebug(ctx, SubsystemHelperResource, msg, additionalFields...) 25 | } 26 | 27 | // HelperResourceWarn emits a helper/resource subsystem log at WARN level. 28 | func HelperResourceWarn(ctx context.Context, msg string, additionalFields ...map[string]interface{}) { 29 | tfsdklog.SubsystemWarn(ctx, SubsystemHelperResource, msg, additionalFields...) 30 | } 31 | 32 | // HelperResourceError emits a helper/resource subsystem log at ERROR level. 33 | func HelperResourceError(ctx context.Context, msg string, additionalFields ...map[string]interface{}) { 34 | tfsdklog.SubsystemError(ctx, SubsystemHelperResource, msg, additionalFields...) 35 | } 36 | -------------------------------------------------------------------------------- /internal/logging/helper_schema.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package logging 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/terraform-plugin-log/tfsdklog" 10 | ) 11 | 12 | const ( 13 | // SubsystemHelperSchema is the tfsdklog subsystem name for helper/schema. 14 | SubsystemHelperSchema = "helper_schema" 15 | ) 16 | 17 | // HelperSchemaDebug emits a helper/schema subsystem log at DEBUG level. 18 | func HelperSchemaDebug(ctx context.Context, msg string, additionalFields ...map[string]interface{}) { 19 | tfsdklog.SubsystemDebug(ctx, SubsystemHelperSchema, msg, additionalFields...) 20 | } 21 | 22 | // HelperSchemaError emits a helper/schema subsystem log at ERROR level. 23 | func HelperSchemaError(ctx context.Context, msg string, additionalFields ...map[string]interface{}) { 24 | tfsdklog.SubsystemError(ctx, SubsystemHelperSchema, msg, additionalFields...) 25 | } 26 | 27 | // HelperSchemaTrace emits a helper/schema subsystem log at TRACE level. 28 | func HelperSchemaTrace(ctx context.Context, msg string, additionalFields ...map[string]interface{}) { 29 | tfsdklog.SubsystemTrace(ctx, SubsystemHelperSchema, msg, additionalFields...) 30 | } 31 | 32 | // HelperSchemaWarn emits a helper/schema subsystem log at WARN level. 33 | func HelperSchemaWarn(ctx context.Context, msg string, additionalFields ...map[string]interface{}) { 34 | tfsdklog.SubsystemWarn(ctx, SubsystemHelperSchema, msg, additionalFields...) 35 | } 36 | -------------------------------------------------------------------------------- /internal/logging/keys.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package logging 5 | 6 | // Structured logging keys. 7 | // 8 | // Practitioners or tooling reading logs may be depending on these keys, so be 9 | // conscious of that when changing them. 10 | // 11 | // Refer to the terraform-plugin-go logging keys as well, which should be 12 | // equivalent to these when possible. 13 | const ( 14 | // Attribute path representation, which is typically in flatmap form such 15 | // as parent.0.child in this project. 16 | KeyAttributePath = "tf_attribute_path" 17 | 18 | // The type of data source being operated on, such as "archive_file" 19 | KeyDataSourceType = "tf_data_source_type" 20 | 21 | // Underlying Go error string when logging an error. 22 | KeyError = "error" 23 | 24 | // The full address of the provider, such as 25 | // registry.terraform.io/hashicorp/random 26 | KeyProviderAddress = "tf_provider_addr" 27 | 28 | // The type of resource being operated on, such as "random_pet" 29 | KeyResourceType = "tf_resource_type" 30 | 31 | // The Deferred reason for an RPC response 32 | KeyDeferredReason = "tf_deferred_reason" 33 | 34 | // The name of the test being executed. 35 | KeyTestName = "test_name" 36 | 37 | // The TestStep number of the test being executed. Starts at 1. 38 | KeyTestStepNumber = "test_step_number" 39 | 40 | // Terraform configuration used during acceptance testing Terraform operations. 41 | KeyTestTerraformConfiguration = "test_terraform_configuration" 42 | 43 | // The Terraform CLI logging level (TF_LOG) used for an acceptance test. 44 | KeyTestTerraformLogLevel = "test_terraform_log_level" 45 | 46 | // The Terraform CLI logging level (TF_LOG_CORE) used for an acceptance test. 47 | KeyTestTerraformLogCoreLevel = "test_terraform_log_core_level" 48 | 49 | // The Terraform CLI logging level (TF_LOG_PROVIDER) used for an acceptance test. 50 | KeyTestTerraformLogProviderLevel = "test_terraform_log_provider_level" 51 | 52 | // The path to the Terraform CLI logging file used for an acceptance test. 53 | // 54 | // This should match where the rest of the acceptance test logs are going 55 | // already, but is provided for troubleshooting in case it does not. 56 | KeyTestTerraformLogPath = "test_terraform_log_path" 57 | 58 | // The path to the Terraform CLI used for an acceptance test. 59 | KeyTestTerraformPath = "test_terraform_path" 60 | 61 | // Terraform plan output generated during a TestStep. 62 | KeyTestTerraformPlan = "test_terraform_plan" 63 | 64 | // The working directory of the acceptance test. 65 | KeyTestWorkingDirectory = "test_working_directory" 66 | ) 67 | -------------------------------------------------------------------------------- /internal/plugintest/config.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugintest 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "os" 10 | "strings" 11 | 12 | "github.com/hashicorp/go-version" 13 | install "github.com/hashicorp/hc-install" 14 | "github.com/hashicorp/hc-install/checkpoint" 15 | "github.com/hashicorp/hc-install/fs" 16 | "github.com/hashicorp/hc-install/product" 17 | "github.com/hashicorp/hc-install/releases" 18 | "github.com/hashicorp/hc-install/src" 19 | "github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging" 20 | ) 21 | 22 | // Config is used to configure the test helper. In most normal test programs 23 | // the configuration is discovered automatically by an Init* function using 24 | // DiscoverConfig, but this is exposed so that more complex scenarios can be 25 | // implemented by direct configuration. 26 | type Config struct { 27 | SourceDir string 28 | TerraformExec string 29 | execTempDir string 30 | PreviousPluginExec string 31 | } 32 | 33 | // DiscoverConfig uses environment variables and other means to automatically 34 | // discover a reasonable test helper configuration. 35 | func DiscoverConfig(ctx context.Context, sourceDir string) (*Config, error) { 36 | tfVersion := strings.TrimPrefix(os.Getenv(EnvTfAccTerraformVersion), "v") 37 | tfPath := os.Getenv(EnvTfAccTerraformPath) 38 | 39 | tempDir := os.Getenv(EnvTfAccTempDir) 40 | tfDir, err := os.MkdirTemp(tempDir, "plugintest-terraform") 41 | if err != nil { 42 | return nil, fmt.Errorf("failed to create temp dir: %w", err) 43 | } 44 | 45 | var sources []src.Source 46 | switch { 47 | case tfPath != "": 48 | logging.HelperResourceTrace(ctx, fmt.Sprintf("Adding potential Terraform CLI source of exact path: %s", tfPath)) 49 | 50 | sources = append(sources, &fs.AnyVersion{ 51 | ExactBinPath: tfPath, 52 | }) 53 | case tfVersion != "": 54 | tfVersion, err := version.NewVersion(tfVersion) 55 | 56 | if err != nil { 57 | return nil, fmt.Errorf("invalid Terraform version: %w", err) 58 | } 59 | 60 | logging.HelperResourceTrace(ctx, fmt.Sprintf("Adding potential Terraform CLI source of releases.hashicorp.com exact version %q for installation in: %s", tfVersion, tfDir)) 61 | 62 | sources = append(sources, &releases.ExactVersion{ 63 | InstallDir: tfDir, 64 | Product: product.Terraform, 65 | Version: tfVersion, 66 | }) 67 | default: 68 | logging.HelperResourceTrace(ctx, "Adding potential Terraform CLI source of local filesystem PATH lookup") 69 | logging.HelperResourceTrace(ctx, fmt.Sprintf("Adding potential Terraform CLI source of checkpoint.hashicorp.com latest version for installation in: %s", tfDir)) 70 | 71 | sources = append(sources, &fs.AnyVersion{ 72 | Product: &product.Terraform, 73 | }) 74 | sources = append(sources, &checkpoint.LatestVersion{ 75 | InstallDir: tfDir, 76 | Product: product.Terraform, 77 | }) 78 | } 79 | 80 | installer := install.NewInstaller() 81 | tfExec, err := installer.Ensure(context.Background(), sources) 82 | if err != nil { 83 | return nil, fmt.Errorf("failed to find or install Terraform CLI from %+v: %w", sources, err) 84 | } 85 | 86 | ctx = logging.TestTerraformPathContext(ctx, tfExec) 87 | 88 | logging.HelperResourceDebug(ctx, "Found Terraform CLI") 89 | 90 | return &Config{ 91 | SourceDir: sourceDir, 92 | TerraformExec: tfExec, 93 | execTempDir: tfDir, 94 | }, nil 95 | } 96 | -------------------------------------------------------------------------------- /internal/plugintest/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package plugintest contains utilities to help with writing tests for 5 | // Terraform plugins. 6 | // 7 | // This is not a package for testing configurations or modules written in the 8 | // Terraform language. It is for testing the plugins that allow Terraform to 9 | // manage various cloud services and other APIs. 10 | package plugintest 11 | -------------------------------------------------------------------------------- /internal/plugintest/guard.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugintest 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | // TestControl is an interface requiring a subset of *testing.T which is used 11 | // by the test guards and helpers in this package. Most callers can simply 12 | // pass their *testing.T value here, but the interface allows other 13 | // implementations to potentially be provided instead, for example to allow 14 | // meta-testing (testing of the test utilities themselves). 15 | // 16 | // This interface also describes the subset of normal test functionality the 17 | // guards and helpers can perform: they can only create log lines, fail tests, 18 | // and skip tests. All other test control is the responsibility of the main 19 | // test code. 20 | type TestControl interface { 21 | Helper() 22 | Log(args ...interface{}) 23 | FailNow() 24 | SkipNow() 25 | Name() string 26 | } 27 | 28 | // testingT wraps a TestControl to recover some of the convenience behaviors 29 | // that would normally come from a real *testing.T, so we can keep TestControl 30 | // small while still having these conveniences. This is an abstraction 31 | // inversion, but accepted because it makes the public API more convenient 32 | // without any considerable disadvantage. 33 | type testingT struct { 34 | TestControl 35 | } 36 | 37 | func (t testingT) Logf(f string, args ...interface{}) { 38 | t.Helper() 39 | t.Log(fmt.Sprintf(f, args...)) 40 | } 41 | 42 | func (t testingT) Fatalf(f string, args ...interface{}) { 43 | t.Helper() 44 | t.Log(fmt.Sprintf(f, args...)) 45 | t.FailNow() 46 | } 47 | 48 | func (t testingT) Skipf(f string, args ...interface{}) { 49 | t.Helper() 50 | t.Log(fmt.Sprintf(f, args...)) 51 | t.SkipNow() 52 | } 53 | -------------------------------------------------------------------------------- /internal/plugintest/util.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugintest 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | ) 11 | 12 | func symlinkFile(src string, dest string) error { 13 | err := os.Symlink(src, dest) 14 | 15 | if err != nil { 16 | return fmt.Errorf("unable to symlink %q to %q: %w", src, dest, err) 17 | } 18 | 19 | srcInfo, err := os.Stat(src) 20 | 21 | if err != nil { 22 | return fmt.Errorf("unable to stat %q: %w", src, err) 23 | } 24 | 25 | err = os.Chmod(dest, srcInfo.Mode()) 26 | 27 | if err != nil { 28 | return fmt.Errorf("unable to set %q permissions: %w", dest, err) 29 | } 30 | 31 | return nil 32 | } 33 | 34 | // symlinkDirectoriesOnly finds only the first-level child directories in srcDir 35 | // and symlinks them into destDir. 36 | // Unlike symlinkDir, this is done non-recursively in order to limit the number 37 | // of file descriptors used. 38 | func symlinkDirectoriesOnly(srcDir string, destDir string) error { 39 | srcInfo, err := os.Stat(srcDir) 40 | if err != nil { 41 | return fmt.Errorf("unable to stat source directory %q: %w", srcDir, err) 42 | } 43 | 44 | err = os.MkdirAll(destDir, srcInfo.Mode()) 45 | if err != nil { 46 | return fmt.Errorf("unable to make destination directory %q: %w", destDir, err) 47 | } 48 | 49 | dirEntries, err := os.ReadDir(srcDir) 50 | 51 | if err != nil { 52 | return fmt.Errorf("unable to read source directory %q: %w", srcDir, err) 53 | } 54 | 55 | for _, dirEntry := range dirEntries { 56 | if !dirEntry.IsDir() { 57 | continue 58 | } 59 | 60 | srcPath := filepath.Join(srcDir, dirEntry.Name()) 61 | destPath := filepath.Join(destDir, dirEntry.Name()) 62 | err := symlinkFile(srcPath, destPath) 63 | 64 | if err != nil { 65 | return fmt.Errorf("unable to symlink directory %q to %q: %w", srcPath, destPath, err) 66 | } 67 | } 68 | 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /internal/plugintest/working_dir_json_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugintest_test 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 11 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 12 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 13 | ) 14 | 15 | // TestJSONConfig verifies that TestStep.Config can contain JSON. 16 | // This test also proves that when changing the HCL and JSON formats back and 17 | // forth, the framework deletes the previous configuration file. 18 | func TestJSONConfig(t *testing.T) { 19 | providerFactories := map[string]func() (*schema.Provider, error){ 20 | "tst": func() (*schema.Provider, error) { return tstProvider(), nil }, //nolint:unparam // required signature 21 | } 22 | resource.Test(t, resource.TestCase{ 23 | IsUnitTest: true, 24 | ProviderFactories: providerFactories, 25 | Steps: []resource.TestStep{{ 26 | Config: `{"resource":{"tst_t":{"r1":{"s":"x1"}}}}`, 27 | Check: resource.TestCheckResourceAttr("tst_t.r1", "s", "x1"), 28 | }, { 29 | Config: `resource "tst_t" "r1" { s = "x2" }`, 30 | Check: resource.TestCheckResourceAttr("tst_t.r1", "s", "x2"), 31 | }, { 32 | Config: `{"resource":{"tst_t":{"r1":{"s":"x3"}}}}`, 33 | Check: resource.TestCheckResourceAttr("tst_t.r1", "s", "x3"), 34 | }}, 35 | }) 36 | } 37 | 38 | func tstProvider() *schema.Provider { 39 | return &schema.Provider{ 40 | ResourcesMap: map[string]*schema.Resource{ 41 | "tst_t": { 42 | CreateContext: resourceTstTCreate, 43 | ReadContext: resourceTstTRead, 44 | UpdateContext: resourceTstTCreate, // Update is the same as Create 45 | DeleteContext: resourceTstTDelete, 46 | Schema: map[string]*schema.Schema{ 47 | "s": { 48 | Type: schema.TypeString, 49 | Required: true, 50 | }, 51 | }, 52 | }, 53 | }, 54 | } 55 | } 56 | 57 | func resourceTstTCreate(ctx context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { 58 | d.SetId(d.Get("s").(string)) 59 | return nil 60 | } 61 | 62 | func resourceTstTRead(ctx context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { 63 | if err := d.Set("s", d.Id()); err != nil { 64 | return diag.FromErr(err) 65 | } 66 | return nil 67 | } 68 | 69 | func resourceTstTDelete(ctx context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { 70 | d.SetId("") 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /internal/providers/provider.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package providers 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/internal/configs/configschema" 8 | ) 9 | 10 | // Schema pairs a provider or resource schema with that schema's version. 11 | // This is used to be able to upgrade the schema in UpgradeResourceState. 12 | type Schema struct { 13 | Version int64 14 | Block *configschema.Block 15 | } 16 | -------------------------------------------------------------------------------- /internal/tfdiags/config_traversals.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfdiags 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "strconv" 10 | 11 | "github.com/hashicorp/go-cty/cty" 12 | ) 13 | 14 | // FormatCtyPath is a helper function to produce a user-friendly string 15 | // representation of a cty.Path. The result uses a syntax similar to the 16 | // HCL expression language in the hope of it being familiar to users. 17 | func FormatCtyPath(path cty.Path) string { 18 | var buf bytes.Buffer 19 | for _, step := range path { 20 | switch ts := step.(type) { 21 | case cty.GetAttrStep: 22 | fmt.Fprintf(&buf, ".%s", ts.Name) 23 | case cty.IndexStep: 24 | buf.WriteByte('[') 25 | key := ts.Key 26 | keyTy := key.Type() 27 | switch { 28 | case key.IsNull(): 29 | buf.WriteString("null") 30 | case !key.IsKnown(): 31 | buf.WriteString("(not yet known)") 32 | case keyTy == cty.Number: 33 | bf := key.AsBigFloat() 34 | buf.WriteString(bf.Text('g', -1)) 35 | case keyTy == cty.String: 36 | buf.WriteString(strconv.Quote(key.AsString())) 37 | default: 38 | buf.WriteString("...") 39 | } 40 | buf.WriteByte(']') 41 | } 42 | } 43 | return buf.String() 44 | } 45 | 46 | // FormatError is a helper function to produce a user-friendly string 47 | // representation of certain special error types that we might want to 48 | // include in diagnostic messages. 49 | // 50 | // This currently has special behavior only for cty.PathError, where a 51 | // non-empty path is rendered in a HCL-like syntax as context. 52 | func FormatError(err error) string { 53 | perr, ok := err.(cty.PathError) 54 | if !ok || len(perr.Path) == 0 { 55 | return err.Error() 56 | } 57 | 58 | return fmt.Sprintf("%s: %s", FormatCtyPath(perr.Path), perr.Error()) 59 | } 60 | -------------------------------------------------------------------------------- /internal/tfdiags/contextual_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfdiags 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/hashicorp/go-cty/cty" 11 | ) 12 | 13 | func TestGetAttribute(t *testing.T) { 14 | path := cty.Path{ 15 | cty.GetAttrStep{Name: "foo"}, 16 | cty.IndexStep{Key: cty.NumberIntVal(0)}, 17 | cty.GetAttrStep{Name: "bar"}, 18 | } 19 | 20 | d := AttributeValue( 21 | Error, 22 | "foo[0].bar", 23 | "detail", 24 | path, 25 | ) 26 | 27 | p := GetAttribute(d) 28 | if !reflect.DeepEqual(path, p) { 29 | t.Fatalf("paths don't match:\nexpected: %#v\ngot: %#v", path, p) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /internal/tfdiags/diagnostic.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfdiags 5 | 6 | type Diagnostic interface { 7 | Severity() Severity 8 | Description() Description 9 | } 10 | 11 | type Severity rune 12 | 13 | // This code was previously generated with a go:generate directive calling: 14 | // go run golang.org/x/tools/cmd/stringer -type=Severity 15 | // However, it is now considered frozen and the tooling dependency has been 16 | // removed. The String method can be manually updated if necessary. 17 | 18 | const ( 19 | Error Severity = 'E' 20 | Warning Severity = 'W' 21 | ) 22 | 23 | type Description struct { 24 | Summary string 25 | Detail string 26 | } 27 | -------------------------------------------------------------------------------- /internal/tfdiags/diagnostic_base.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfdiags 5 | 6 | // diagnosticBase can be embedded in other diagnostic structs to get 7 | // default implementations of Severity and Description. This type also 8 | // has default implementations of Source that return no source 9 | // location or expression-related information, so embedders should generally 10 | // override those method to return more useful results where possible. 11 | type diagnosticBase struct { 12 | severity Severity 13 | summary string 14 | detail string 15 | } 16 | 17 | func (d diagnosticBase) Severity() Severity { 18 | return d.severity 19 | } 20 | 21 | func (d diagnosticBase) Description() Description { 22 | return Description{ 23 | Summary: d.summary, 24 | Detail: d.detail, 25 | } 26 | } 27 | 28 | func Diag(sev Severity, summary, detail string) Diagnostic { 29 | return &diagnosticBase{ 30 | severity: sev, 31 | summary: summary, 32 | detail: detail, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/tfdiags/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package tfdiags is a utility package for representing errors and 5 | // warnings in a manner that allows us to produce good messages for the 6 | // user. 7 | // 8 | // "diag" is short for "diagnostics", and is meant as a general word for 9 | // feedback to a user about potential or actual problems. 10 | // 11 | // A design goal for this package is for it to be able to provide rich 12 | // messaging where possible but to also be pragmatic about dealing with 13 | // generic errors produced by system components that _can't_ provide 14 | // such rich messaging. As a consequence, the main types in this package -- 15 | // Diagnostics and Diagnostic -- are designed so that they can be "smuggled" 16 | // over an error channel and then be unpacked at the other end, so that 17 | // error diagnostics (at least) can transit through APIs that are not 18 | // aware of this package. 19 | package tfdiags 20 | -------------------------------------------------------------------------------- /internal/tfdiags/error.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfdiags 5 | 6 | // nativeError is a Diagnostic implementation that wraps a normal Go error 7 | type nativeError struct { 8 | err error 9 | } 10 | 11 | var _ Diagnostic = nativeError{} 12 | 13 | func (e nativeError) Severity() Severity { 14 | return Error 15 | } 16 | 17 | func (e nativeError) Description() Description { 18 | return Description{ 19 | Summary: FormatError(e.err), 20 | } 21 | } 22 | 23 | func FromError(err error) Diagnostic { 24 | return &nativeError{ 25 | err: err, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /internal/tfdiags/severity_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=Severity"; DO NOT EDIT. 2 | 3 | package tfdiags 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[Error-69] 12 | _ = x[Warning-87] 13 | } 14 | 15 | const ( 16 | _Severity_name_0 = "Error" 17 | _Severity_name_1 = "Warning" 18 | ) 19 | 20 | func (i Severity) String() string { 21 | switch { 22 | case i == 69: 23 | return _Severity_name_0 24 | case i == 87: 25 | return _Severity_name_1 26 | default: 27 | return "Severity(" + strconv.FormatInt(int64(i), 10) + ")" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/tfdiags/simple_warning.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfdiags 5 | 6 | type simpleWarning string 7 | 8 | var _ Diagnostic = simpleWarning("") 9 | 10 | // SimpleWarning constructs a simple (summary-only) warning diagnostic. 11 | func SimpleWarning(msg string) Diagnostic { 12 | return simpleWarning(msg) 13 | } 14 | 15 | func (e simpleWarning) Severity() Severity { 16 | return Warning 17 | } 18 | 19 | func (e simpleWarning) Description() Description { 20 | return Description{ 21 | Summary: string(e), 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /meta/meta.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // The meta package provides a location to set the release version 5 | // and any other relevant metadata for the SDK. 6 | // 7 | // This package should not import any other SDK packages. 8 | package meta 9 | 10 | import ( 11 | "fmt" 12 | 13 | version "github.com/hashicorp/go-version" 14 | ) 15 | 16 | // The main version number that is being run at the moment. 17 | // 18 | // Deprecated: Use Go standard library [runtime/debug] package build information 19 | // instead. 20 | var SDKVersion = "2.37.0" 21 | 22 | // A pre-release marker for the version. If this is "" (empty string) 23 | // then it means that it is a final release. Otherwise, this is a pre-release 24 | // such as "dev" (in development), "beta", "rc1", etc. 25 | // 26 | // Deprecated: Use Go standard library [runtime/debug] package build information 27 | // instead. 28 | var SDKPrerelease = "" 29 | 30 | // SemVer is an instance of version.Version. This has the secondary 31 | // benefit of verifying during tests and init time that our version is a 32 | // proper semantic version, which should always be the case. 33 | var SemVer *version.Version 34 | 35 | func init() { 36 | SemVer = version.Must(version.NewVersion(SDKVersion)) 37 | } 38 | 39 | // VersionString returns the complete version string, including prerelease 40 | // 41 | // Deprecated: Use Go standard library [runtime/debug] package build information 42 | // instead. 43 | func SDKVersionString() string { 44 | if SDKPrerelease != "" { 45 | return fmt.Sprintf("%s-%s", SDKVersion, SDKPrerelease) 46 | } 47 | return SDKVersion 48 | } 49 | -------------------------------------------------------------------------------- /plugin/debug.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugin 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "time" 10 | 11 | "github.com/hashicorp/go-plugin" 12 | ) 13 | 14 | // ReattachConfig holds the information Terraform needs to be able to attach 15 | // itself to a provider process, so it can drive the process. 16 | type ReattachConfig struct { 17 | Protocol string 18 | ProtocolVersion int 19 | Pid int 20 | Test bool 21 | Addr ReattachConfigAddr 22 | } 23 | 24 | // ReattachConfigAddr is a JSON-encoding friendly version of net.Addr. 25 | type ReattachConfigAddr struct { 26 | Network string 27 | String string 28 | } 29 | 30 | // DebugServe starts a plugin server in debug mode; this should only be used 31 | // when the provider will manage its own lifecycle. It is not recommended for 32 | // normal usage; Serve is the correct function for that. 33 | func DebugServe(ctx context.Context, opts *ServeOpts) (ReattachConfig, <-chan struct{}, error) { 34 | reattachCh := make(chan *plugin.ReattachConfig) 35 | closeCh := make(chan struct{}) 36 | 37 | if opts == nil { 38 | return ReattachConfig{}, closeCh, errors.New("ServeOpts must be passed in with at least GRPCProviderFunc, GRPCProviderV6Func, or ProviderFunc") 39 | } 40 | 41 | opts.TestConfig = &plugin.ServeTestConfig{ 42 | Context: ctx, 43 | ReattachConfigCh: reattachCh, 44 | CloseCh: closeCh, 45 | } 46 | 47 | go Serve(opts) 48 | 49 | var config *plugin.ReattachConfig 50 | select { 51 | case config = <-reattachCh: 52 | case <-time.After(2 * time.Second): 53 | return ReattachConfig{}, closeCh, errors.New("timeout waiting on reattach config") 54 | } 55 | 56 | if config == nil { 57 | return ReattachConfig{}, closeCh, errors.New("nil reattach config received") 58 | } 59 | 60 | return ReattachConfig{ 61 | Protocol: string(config.Protocol), 62 | ProtocolVersion: config.ProtocolVersion, 63 | Pid: config.Pid, 64 | Test: config.Test, 65 | Addr: ReattachConfigAddr{ 66 | Network: config.Addr.Network(), 67 | String: config.Addr.String(), 68 | }, 69 | }, closeCh, nil 70 | } 71 | 72 | // Debug starts a debug server and controls its lifecycle, printing the 73 | // information needed for Terraform to connect to the provider to stdout. 74 | // os.Interrupt will be captured and used to stop the server. 75 | // 76 | // Deprecated: Use the Serve function with the ServeOpts Debug field instead. 77 | func Debug(ctx context.Context, providerAddr string, opts *ServeOpts) error { 78 | if opts == nil { 79 | return errors.New("ServeOpts must be passed in with at least GRPCProviderFunc, GRPCProviderV6Func, or ProviderFunc") 80 | } 81 | 82 | opts.Debug = true 83 | 84 | if opts.ProviderAddr == "" { 85 | opts.ProviderAddr = providerAddr 86 | } 87 | 88 | Serve(opts) 89 | 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /terraform/instancetype.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package terraform 5 | 6 | // This code was previously generated with a go:generate directive calling: 7 | // go run golang.org/x/tools/cmd/stringer -type=instanceType instancetype.go 8 | // However, it is now considered frozen and the tooling dependency has been 9 | // removed. The String method can be manually updated if necessary. 10 | 11 | // instanceType is an enum of the various types of instances store in the State 12 | type instanceType int 13 | 14 | const ( 15 | typeInvalid instanceType = iota 16 | typePrimary 17 | typeTainted 18 | typeDeposed 19 | ) 20 | -------------------------------------------------------------------------------- /terraform/instancetype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=instanceType instancetype.go"; DO NOT EDIT. 2 | 3 | package terraform 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[typeInvalid-0] 12 | _ = x[typePrimary-1] 13 | _ = x[typeTainted-2] 14 | _ = x[typeDeposed-3] 15 | } 16 | 17 | const _instanceType_name = "typeInvalidtypePrimarytypeTaintedtypeDeposed" 18 | 19 | var _instanceType_index = [...]uint8{0, 11, 22, 33, 44} 20 | 21 | func (i instanceType) String() string { 22 | if i < 0 || i >= instanceType(len(_instanceType_index)-1) { 23 | return "instanceType(" + strconv.FormatInt(int64(i), 10) + ")" 24 | } 25 | return _instanceType_name[_instanceType_index[i]:_instanceType_index[i+1]] 26 | } 27 | -------------------------------------------------------------------------------- /terraform/resource_mode.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package terraform 5 | 6 | // This code was previously generated with a go:generate directive calling: 7 | // go run golang.org/x/tools/cmd/stringer -type=ResourceMode -output=resource_mode_string.go resource_mode.go 8 | // However, it is now considered frozen and the tooling dependency has been 9 | // removed. The String method can be manually updated if necessary. 10 | 11 | // ResourceMode is deprecated, use addrs.ResourceMode instead. 12 | // It has been preserved for backwards compatibility. 13 | type ResourceMode int 14 | 15 | const ( 16 | ManagedResourceMode ResourceMode = iota 17 | DataResourceMode 18 | ) 19 | -------------------------------------------------------------------------------- /terraform/resource_mode_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=ResourceMode -output=resource_mode_string.go resource_mode.go"; DO NOT EDIT. 2 | 3 | package terraform 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[ManagedResourceMode-0] 12 | _ = x[DataResourceMode-1] 13 | } 14 | 15 | const _ResourceMode_name = "ManagedResourceModeDataResourceMode" 16 | 17 | var _ResourceMode_index = [...]uint8{0, 19, 35} 18 | 19 | func (i ResourceMode) String() string { 20 | if i < 0 || i >= ResourceMode(len(_ResourceMode_index)-1) { 21 | return "ResourceMode(" + strconv.FormatInt(int64(i), 10) + ")" 22 | } 23 | return _ResourceMode_name[_ResourceMode_index[i]:_ResourceMode_index[i+1]] 24 | } 25 | -------------------------------------------------------------------------------- /terraform/resource_provider.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package terraform 5 | 6 | // ResourceType is a type of resource that a resource provider can manage. 7 | type ResourceType struct { 8 | Name string // Name of the resource, example "instance" (no provider prefix) 9 | Importable bool // Whether this resource supports importing 10 | 11 | // SchemaAvailable is set if the provider supports the ProviderSchema, 12 | // ResourceTypeSchema and DataSourceSchema methods. Although it is 13 | // included on each resource type, it's actually a provider-wide setting 14 | // that's smuggled here only because that avoids a breaking change to 15 | // the plugin protocol. 16 | SchemaAvailable bool 17 | } 18 | 19 | // DataSource is a data source that a resource provider implements. 20 | type DataSource struct { 21 | Name string 22 | 23 | // SchemaAvailable is set if the provider supports the ProviderSchema, 24 | // ResourceTypeSchema and DataSourceSchema methods. Although it is 25 | // included on each resource type, it's actually a provider-wide setting 26 | // that's smuggled here only because that avoids a breaking change to 27 | // the plugin protocol. 28 | SchemaAvailable bool 29 | } 30 | -------------------------------------------------------------------------------- /terraform/schemas.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package terraform 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/internal/configs/configschema" 8 | ) 9 | 10 | // ProviderSchema represents the schema for a provider's own configuration 11 | // and the configuration for some or all of its resources and data sources. 12 | // 13 | // The completeness of this structure depends on how it was constructed. 14 | // When constructed for a configuration, it will generally include only 15 | // resource types and data sources used by that configuration. 16 | type ProviderSchema struct { 17 | Provider *configschema.Block 18 | ResourceTypes map[string]*configschema.Block 19 | DataSources map[string]*configschema.Block 20 | 21 | ResourceTypeSchemaVersions map[string]uint64 22 | } 23 | 24 | // ProviderSchemaRequest is used to describe to a ResourceProvider which 25 | // aspects of schema are required, when calling the GetSchema method. 26 | type ProviderSchemaRequest struct { 27 | ResourceTypes []string 28 | DataSources []string 29 | } 30 | -------------------------------------------------------------------------------- /terraform/util.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package terraform 5 | 6 | import ( 7 | "sort" 8 | ) 9 | 10 | // deduplicate a slice of strings 11 | func uniqueStrings(s []string) []string { 12 | if len(s) < 2 { 13 | return s 14 | } 15 | 16 | sort.Strings(s) 17 | result := make([]string, 1, len(s)) 18 | result[0] = s[0] 19 | for i := 1; i < len(s); i++ { 20 | if s[i] != result[len(result)-1] { 21 | result = append(result, s[i]) 22 | } 23 | } 24 | return result 25 | } 26 | -------------------------------------------------------------------------------- /terraform/util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package terraform 5 | 6 | import ( 7 | "fmt" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestUniqueStrings(t *testing.T) { 13 | cases := []struct { 14 | Input []string 15 | Expected []string 16 | }{ 17 | { 18 | []string{}, 19 | []string{}, 20 | }, 21 | { 22 | []string{"x"}, 23 | []string{"x"}, 24 | }, 25 | { 26 | []string{"a", "b", "c"}, 27 | []string{"a", "b", "c"}, 28 | }, 29 | { 30 | []string{"a", "a", "a"}, 31 | []string{"a"}, 32 | }, 33 | { 34 | []string{"a", "b", "a", "b", "a", "a"}, 35 | []string{"a", "b"}, 36 | }, 37 | { 38 | []string{"c", "b", "a", "c", "b"}, 39 | []string{"a", "b", "c"}, 40 | }, 41 | } 42 | 43 | for i, tc := range cases { 44 | t.Run(fmt.Sprintf("unique-%d", i), func(t *testing.T) { 45 | actual := uniqueStrings(tc.Input) 46 | if !reflect.DeepEqual(tc.Expected, actual) { 47 | t.Fatalf("Expected: %q\nGot: %q", tc.Expected, actual) 48 | } 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tools/copywrite.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build generate 5 | 6 | package tools 7 | 8 | import ( 9 | // copywrite header generation 10 | _ "github.com/hashicorp/copywrite" 11 | ) 12 | 13 | //go:generate go run github.com/hashicorp/copywrite headers -d .. --config ../.copywrite.hcl 14 | -------------------------------------------------------------------------------- /tools/go.mod: -------------------------------------------------------------------------------- 1 | module tools 2 | 3 | go 1.23.7 4 | 5 | require github.com/hashicorp/copywrite v0.22.0 6 | 7 | require ( 8 | github.com/AlecAivazis/survey/v2 v2.3.7 // indirect 9 | github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect 10 | github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect 11 | github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect 12 | github.com/bradleyfalzon/ghinstallation/v2 v2.5.0 // indirect 13 | github.com/cli/go-gh/v2 v2.12.1 // indirect 14 | github.com/cli/safeexec v1.0.0 // indirect 15 | github.com/cloudflare/circl v1.3.7 // indirect 16 | github.com/fatih/color v1.13.0 // indirect 17 | github.com/fsnotify/fsnotify v1.5.4 // indirect 18 | github.com/go-openapi/errors v0.20.2 // indirect 19 | github.com/go-openapi/strfmt v0.21.3 // indirect 20 | github.com/golang-jwt/jwt/v4 v4.5.2 // indirect 21 | github.com/golang/protobuf v1.5.2 // indirect 22 | github.com/google/go-github/v45 v45.2.0 // indirect 23 | github.com/google/go-github/v53 v53.0.0 // indirect 24 | github.com/google/go-querystring v1.1.0 // indirect 25 | github.com/hashicorp/go-hclog v1.5.0 // indirect 26 | github.com/hashicorp/hcl v1.0.0 // indirect 27 | github.com/inconshreveable/mousetrap v1.0.1 // indirect 28 | github.com/jedib0t/go-pretty v4.3.0+incompatible // indirect 29 | github.com/jedib0t/go-pretty/v6 v6.4.6 // indirect 30 | github.com/joho/godotenv v1.3.0 // indirect 31 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 32 | github.com/knadh/koanf v1.5.0 // indirect 33 | github.com/mattn/go-colorable v0.1.13 // indirect 34 | github.com/mattn/go-isatty v0.0.20 // indirect 35 | github.com/mattn/go-runewidth v0.0.16 // indirect 36 | github.com/mergestat/timediff v0.0.3 // indirect 37 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 38 | github.com/mitchellh/copystructure v1.2.0 // indirect 39 | github.com/mitchellh/go-homedir v1.1.0 // indirect 40 | github.com/mitchellh/mapstructure v1.5.0 // indirect 41 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 42 | github.com/oklog/ulid v1.3.1 // indirect 43 | github.com/rivo/uniseg v0.4.7 // indirect 44 | github.com/rogpeppe/go-internal v1.10.0 // indirect 45 | github.com/samber/lo v1.37.0 // indirect 46 | github.com/spf13/cobra v1.6.1 // indirect 47 | github.com/spf13/pflag v1.0.5 // indirect 48 | github.com/thanhpk/randstr v1.0.4 // indirect 49 | go.mongodb.org/mongo-driver v1.10.0 // indirect 50 | golang.org/x/crypto v0.36.0 // indirect 51 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect 52 | golang.org/x/net v0.38.0 // indirect 53 | golang.org/x/oauth2 v0.8.0 // indirect 54 | golang.org/x/sync v0.12.0 // indirect 55 | golang.org/x/sys v0.31.0 // indirect 56 | golang.org/x/term v0.30.0 // indirect 57 | golang.org/x/text v0.23.0 // indirect 58 | google.golang.org/appengine v1.6.7 // indirect 59 | google.golang.org/protobuf v1.33.0 // indirect 60 | gopkg.in/yaml.v3 v3.0.1 // indirect 61 | ) 62 | -------------------------------------------------------------------------------- /website/Makefile: -------------------------------------------------------------------------------- 1 | ###################################################### 2 | # NOTE: This file is managed by the Digital Team's # 3 | # Terraform configuration @ hashicorp/mktg-terraform # 4 | ###################################################### 5 | 6 | .DEFAULT_GOAL := website 7 | 8 | # Set the preview mode for the website shell to "developer" or "io" 9 | PREVIEW_MODE ?= developer 10 | REPO ?= terraform-plugin-sdk 11 | 12 | # Enable setting alternate docker tool, e.g. 'make DOCKER_CMD=podman' 13 | DOCKER_CMD ?= docker 14 | 15 | CURRENT_GIT_BRANCH=$$(git rev-parse --abbrev-ref HEAD) 16 | LOCAL_CONTENT_DIR=../docs 17 | PWD=$$(pwd) 18 | 19 | DOCKER_IMAGE="hashicorp/dev-portal" 20 | DOCKER_IMAGE_LOCAL="dev-portal-local" 21 | DOCKER_RUN_FLAGS=-it \ 22 | --publish "3000:3000" \ 23 | --rm \ 24 | --tty \ 25 | --volume "$(PWD)/docs:/app/docs" \ 26 | --volume "$(PWD)/img:/app/public" \ 27 | --volume "$(PWD)/data:/app/data" \ 28 | --volume "$(PWD)/redirects.js:/app/redirects.js" \ 29 | --volume "next-dir:/app/website-preview/.next" \ 30 | --volume "$(PWD)/.env:/app/.env" \ 31 | --volume "$(PWD)/.env.development:/app/website-preview/.env.development" \ 32 | --volume "$(PWD)/.env.local:/app/website-preview/.env.local" \ 33 | -e "REPO=$(REPO)" \ 34 | -e "PREVIEW_FROM_REPO=$(REPO)" \ 35 | -e "IS_CONTENT_PREVIEW=true" \ 36 | -e "LOCAL_CONTENT_DIR=$(LOCAL_CONTENT_DIR)" \ 37 | -e "CURRENT_GIT_BRANCH=$(CURRENT_GIT_BRANCH)" \ 38 | -e "PREVIEW_MODE=$(PREVIEW_MODE)" 39 | 40 | # Default: run this if working on the website locally to run in watch mode. 41 | .PHONY: website 42 | website: 43 | @echo "==> Downloading latest Docker image..." 44 | @$(DOCKER_CMD) pull $(DOCKER_IMAGE) 45 | @echo "==> Starting website..." 46 | @$(DOCKER_CMD) run $(DOCKER_RUN_FLAGS) $(DOCKER_IMAGE) 47 | 48 | # Use this if you have run `website/build-local` to use the locally built image. 49 | .PHONY: website/local 50 | website/local: 51 | @echo "==> Starting website from local image..." 52 | @$(DOCKER_CMD) run $(DOCKER_RUN_FLAGS) $(DOCKER_IMAGE_LOCAL) 53 | 54 | # Run this to generate a new local Docker image. 55 | .PHONY: website/build-local 56 | website/build-local: 57 | @echo "==> Building local Docker image" 58 | @$(DOCKER_CMD) build https://github.com/hashicorp/dev-portal.git\#main \ 59 | -t $(DOCKER_IMAGE_LOCAL) 60 | 61 | -------------------------------------------------------------------------------- /website/docs/plugin/sdkv2/debugging.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: Plugin Development - Debugging SDKv2 Providers 3 | description: How to implement debugger support in SDKv2 Terraform providers. 4 | --- 5 | 6 | > [!IMPORTANT] 7 | > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. 8 | 9 | # Debugging SDKv2 Providers 10 | 11 | This page contains implementation details for inspecting runtime information of a Terraform provider developed with SDKv2 via a debugger tool. Review the top level [Debugging](/terraform/plugin/debugging) page for information pertaining to the overall Terraform provider debugging process and other inspection options, such as log-based debugging. 12 | 13 | ## Code Implementation 14 | 15 | Update the `main` function for the project to conditionally enable the [`plugin/ServeOpts.Debug` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/plugin#ServeOpts.Debug). Conventionally, a `-debug` flag is used to control the `Debug` value. 16 | 17 | This example uses a `-debug` flag to enable debugging, otherwise starting the provider normally: 18 | 19 | ```go 20 | func main() { 21 | var debug bool 22 | 23 | flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve") 24 | flag.Parse() 25 | 26 | opts := &plugin.ServeOpts{ 27 | Debug: debug, 28 | ProviderAddr: "registry.terraform.io/example-namespace/example", 29 | ProviderFunc: provider.New(), 30 | } 31 | 32 | plugin.Serve(opts) 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /website/docs/plugin/sdkv2/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: 'Home - Plugin Development: SDKv2' 3 | description: Maintain plugins built on the legacy SDK. 4 | --- 5 | 6 | > [!IMPORTANT] 7 | > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. 8 | 9 | # Terraform Plugin SDKv2 10 | 11 | Terraform Plugin SDKv2 is a way to maintain Terraform Plugins on [protocol version 5](/terraform/plugin/terraform-plugin-protocol#protocol-version-5). 12 | 13 | We recommend using the [framework](/terraform/plugin/framework) to develop new provider functionality because it offers significant advantages as compared to the SDKv2. We also recommend migrating existing providers to the framework when possible. Refer to [Plugin Framework Benefits](/terraform/plugin/framework-benefits) for higher level details about how the framework makes provider development easier and [Plugin Framework Features](/terraform/plugin/framework/migrating/benefits) for a detailed functionality comparison between the SDKv2 and the framework. 14 | 15 | ## Maintain Existing Providers 16 | 17 | - Try the [Call APIs with Custom Providers](/terraform/tutorials/providers) tutorials. 18 | - Clone the [terraform-provider-scaffolding](https://github.com/hashicorp/terraform-provider-scaffolding) template repository on GitHub. 19 | - Review the SDKv2 documentation for key concepts such as [Schemas](/terraform/plugin/sdkv2/schemas) and [Resources](/terraform/plugin/sdkv2/resources). 20 | 21 | ## Debug and Test 22 | 23 | - Learn how to [debug your provider](/terraform/plugin/sdkv2/debugging) using either logging calls or a debugging tool. 24 | - Write [acceptance and unit tests](/terraform/plugin/sdkv2/testing) for your provider. 25 | 26 | ## Migrate, Combine, or Translate 27 | 28 | - Use the [framework migration guide](/terraform/plugin/framework/migrating) to migrate existing providers from SDKv2 to the framework. You can use `terraform-plugin-mux` to migrate individual resources and data sources slowly over time. 29 | - [Combine your provider](/terraform/plugin/mux/combining-protocol-version-5-providers) with other [protocol version 5](/terraform/plugin/terraform-plugin-protocol#protocol-version-5) providers. 30 | - [Translate your provider](/terraform/plugin/mux/translating-protocol-version-5-to-6) into a [protocol version 6](/terraform/plugin/terraform-plugin-protocol#protocol-version-6) provider to require Terraform 1.0 and later or to combine with a framework provider and use protocol version 6 exclusive functionality such as nested attributes. 31 | -------------------------------------------------------------------------------- /website/docs/plugin/sdkv2/logging/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: Plugin Development - Logging 3 | description: |- 4 | High-quality logs are important when debugging your provider. Learn to set-up logging and write meaningful logs. 5 | --- 6 | 7 | > [!IMPORTANT] 8 | > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. 9 | 10 | # Logging 11 | 12 | Terraform Plugin SDKv2 integrates with the structured logging framework [terraform-plugin-log](/terraform/plugin/log). High-quality logs are critical to quickly [debugging your provider](/terraform/plugin/debugging). 13 | 14 | ## Managing Log Output 15 | 16 | Learn how to use [environment variables and other methods](/terraform/plugin/log/managing) to enable and filter logs. 17 | 18 | ## Writing Log Output 19 | 20 | Learn how to [implement code in provider logic](/terraform/plugin/log/writing) to output logs. 21 | 22 | ## Filtering Log Output 23 | 24 | Learn how to [implement code in provider logic](/terraform/plugin/log/filtering) to omit logs or mask specific log messages and structured log fields. 25 | 26 | ## Log HTTP Transactions 27 | 28 | Learn how to [set up the Logging HTTP Transport](/terraform/plugin/sdkv2/logging/http-transport) to log HTTP Transactions with the [structured logging framework](/terraform/plugin/log). 29 | -------------------------------------------------------------------------------- /website/docs/plugin/sdkv2/schemas/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: Plugin Development - Schemas 3 | description: |- 4 | Schemas define plugin attributes and behaviors. Learn how to create schemas 5 | in SDKv2. 6 | --- 7 | 8 | > [!IMPORTANT] 9 | > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. 10 | 11 | # Terraform Schemas 12 | 13 | Terraform Plugins are expressed using schemas to define attributes and their 14 | behaviors, using a high level package exposed by Terraform Core named 15 | [`schema`](https://github.com/hashicorp/terraform-plugin-sdk/tree/main/helper/schema). 16 | Providers, Resources, and Provisioners all contains schemas, and Terraform Core 17 | uses them to produce plan and apply executions based on the behaviors described. 18 | 19 | Below is an example `provider.go` file, detailing a hypothetical `ExampleProvider` implementation: 20 | 21 | ```go 22 | package exampleprovider 23 | 24 | import ( 25 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 26 | ) 27 | 28 | func Provider() *schema.Provider { 29 | // Example Provider requires an API Token. 30 | // The Email is optional 31 | return &schema.Provider{ 32 | Schema: map[string]*schema.Schema{ 33 | "api_token": { 34 | Type: schema.TypeString, 35 | Required: true, 36 | }, 37 | "email": { 38 | Type: schema.TypeString, 39 | Optional: true, 40 | Default: "", 41 | }, 42 | }, 43 | } 44 | } 45 | ``` 46 | 47 | In this example we’re creating a `Provider` and setting it’s `schema`. This 48 | schema is a collection of key value pairs of schema elements the attributes a 49 | user can specify in their configuration. The keys are strings, and the values 50 | are 51 | [`schema.Schema`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema#Schema) 52 | structs that define the behavior. 53 | 54 | Schemas can be thought of as a type paired one or more properties that describe 55 | it’s behavior. 56 | 57 | ## Schema Types 58 | 59 | Schema items must be defined using one of the builtin types, such as 60 | `TypeString`, `TypeBool`, `TypeInt`, et. al. The type defines what is considered 61 | valid input for a given schema item in a users configuration. 62 | 63 | See [Schema Types](/terraform/plugin/sdkv2/schemas/schema-types) for more 64 | information on the types available to schemas. 65 | 66 | ## Schema Behaviors 67 | 68 | Schema items can have various properties that can be combined to match their 69 | behaviors represented by their API. Some items are **Required**, others 70 | **Optional**, while others may be **Computed** such that they are useful to be 71 | tracked in state, but cannot be configured by users. 72 | 73 | See [Schema Behaviors](/terraform/plugin/sdkv2/schemas/schema-behaviors) for more 74 | information on the properties a schema can have. 75 | -------------------------------------------------------------------------------- /website/docs/plugin/sdkv2/testing/testing-api.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: Plugin Development - Testing API 3 | description: |- 4 | Plugin Development is a section for content dedicated to developing Plugins 5 | to extend Terraform's core offering. 6 | --- 7 | 8 | > [!IMPORTANT] 9 | > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. 10 | 11 | # Testing API 12 | -------------------------------------------------------------------------------- /website/docs/plugin/sdkv2/testing/testing-patterns.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: Plugin Development - Testing Patterns 3 | description: |- 4 | Plugin Development is a section for content dedicated to developing Plugins 5 | to extend Terraform's core offering. 6 | --- 7 | 8 | > [!IMPORTANT] 9 | > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. 10 | 11 | # Testing Patterns 12 | -------------------------------------------------------------------------------- /website/img/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp/terraform-plugin-sdk/289cf91e0721406e2c9c0122cc7685cab7a7df27/website/img/.gitkeep -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "terraform-plugin-sdk-docs-preview", 3 | "private": "true", 4 | "scripts": { 5 | "build": "./scripts/website-build.sh", 6 | "content-check": "hc-content --config base-docs" 7 | }, 8 | "devDependencies": { 9 | "@hashicorp/platform-content-conformance": "^0.0.8", 10 | "next": "^13.5.4" 11 | }, 12 | "engines": { 13 | "npm": ">=7.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /website/scripts/should-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | ###################################################### 7 | # NOTE: This file is managed by the Digital Team's # 8 | # Terraform configuration @ hashicorp/mktg-terraform # 9 | ###################################################### 10 | 11 | # This is run during the website build step to determine if we should skip the build or not. 12 | # More information: https://vercel.com/docs/platform/projects#ignored-build-step 13 | 14 | if [[ "$VERCEL_GIT_COMMIT_REF" == "stable-website" ]] ; then 15 | # Proceed with the build if the branch is stable-website 16 | echo "✅ - Build can proceed" 17 | exit 1; 18 | else 19 | # Check for differences in the website directory 20 | git diff --quiet HEAD^ HEAD ./ 21 | fi -------------------------------------------------------------------------------- /website/scripts/website-build.sh: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | ###################################################### 5 | # NOTE: This file is managed by the Digital Team's # 6 | # Terraform configuration @ hashicorp/mktg-terraform # 7 | ###################################################### 8 | 9 | # Repo which we are cloning and executing npm run build:deploy-preview within 10 | REPO_TO_CLONE=dev-portal 11 | # Set the subdirectory name for the base project 12 | PREVIEW_DIR=website-preview 13 | # The directory we want to clone the project into 14 | CLONE_DIR=website-preview 15 | # The product for which we are building the deploy preview 16 | PRODUCT=terraform-plugin-sdk 17 | # Preview mode, controls the UI rendered (either the product site or developer). Can be `io` or `developer` 18 | PREVIEW_MODE=developer 19 | 20 | # Get the git branch of the commit that triggered the deploy preview 21 | # This will power remote image assets in local and deploy previews 22 | CURRENT_GIT_BRANCH=$VERCEL_GIT_COMMIT_REF 23 | 24 | # This is where content files live, relative to the website-preview dir. If omitted, "../content" will be used 25 | LOCAL_CONTENT_DIR=../docs 26 | 27 | from_cache=false 28 | 29 | if [ -d "$PREVIEW_DIR" ]; then 30 | echo "$PREVIEW_DIR found" 31 | CLONE_DIR="$PREVIEW_DIR-tmp" 32 | from_cache=true 33 | fi 34 | 35 | # Clone the base project, if needed 36 | echo "⏳ Cloning the $REPO_TO_CLONE repo, this might take a while..." 37 | git clone --depth=1 "https://github.com/hashicorp/$REPO_TO_CLONE.git" "$CLONE_DIR" 38 | 39 | if [ "$from_cache" = true ]; then 40 | echo "Setting up $PREVIEW_DIR" 41 | cp -R "./$CLONE_DIR/." "./$PREVIEW_DIR" 42 | fi 43 | 44 | # cd into the preview directory project 45 | cd "$PREVIEW_DIR" 46 | 47 | # Run the build:deploy-preview start script 48 | PREVIEW_FROM_REPO=$PRODUCT \ 49 | IS_CONTENT_PREVIEW=true \ 50 | PREVIEW_MODE=$PREVIEW_MODE \ 51 | REPO=$PRODUCT \ 52 | HASHI_ENV=project-preview \ 53 | LOCAL_CONTENT_DIR=$LOCAL_CONTENT_DIR \ 54 | CURRENT_GIT_BRANCH=$CURRENT_GIT_BRANCH \ 55 | npm run build:deploy-preview -------------------------------------------------------------------------------- /website/scripts/website-start.sh: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | ###################################################### 5 | # NOTE: This file is managed by the Digital Team's # 6 | # Terraform configuration @ hashicorp/mktg-terraform # 7 | ###################################################### 8 | 9 | # Repo which we are cloning and executing npm run build:deploy-preview within 10 | REPO_TO_CLONE=dev-portal 11 | # Set the subdirectory name for the dev-portal app 12 | PREVIEW_DIR=website-preview 13 | # The product for which we are building the deploy preview 14 | PRODUCT=terraform-plugin-sdk 15 | # Preview mode, controls the UI rendered (either the product site or developer). Can be `io` or `developer` 16 | PREVIEW_MODE=developer 17 | 18 | # Get the git branch of the commit that triggered the deploy preview 19 | # This will power remote image assets in local and deploy previews 20 | CURRENT_GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) 21 | 22 | # This is where content files live, relative to the website-preview dir. If omitted, "../content" will be used 23 | LOCAL_CONTENT_DIR=../docs 24 | 25 | should_pull=true 26 | 27 | # Clone the dev-portal project, if needed 28 | if [ ! -d "$PREVIEW_DIR" ]; then 29 | echo "⏳ Cloning the $REPO_TO_CLONE repo, this might take a while..." 30 | git clone --depth=1 https://github.com/hashicorp/$REPO_TO_CLONE.git "$PREVIEW_DIR" 31 | should_pull=false 32 | fi 33 | 34 | cd "$PREVIEW_DIR" 35 | 36 | # If the directory already existed, pull to ensure the clone is fresh 37 | if [ "$should_pull" = true ]; then 38 | git pull origin main 39 | fi 40 | 41 | # Run the dev-portal content-repo start script 42 | REPO=$PRODUCT \ 43 | PREVIEW_FROM_REPO=$PRODUCT \ 44 | LOCAL_CONTENT_DIR=$LOCAL_CONTENT_DIR \ 45 | CURRENT_GIT_BRANCH=$CURRENT_GIT_BRANCH \ 46 | PREVIEW_MODE=$PREVIEW_MODE \ 47 | npm run start:local-preview --------------------------------------------------------------------------------