├── .go-version ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── pull_request_template.md ├── SECURITY.md ├── dependabot.yml ├── workflows │ ├── release.yml │ └── test.yml └── CONTRIBUTING.MD ├── internal ├── provider │ ├── testdata │ │ ├── complex │ │ │ ├── delete │ │ │ │ └── main.tf │ │ │ ├── create │ │ │ │ └── main.tf │ │ │ └── update │ │ │ │ └── main.tf │ │ ├── dynamic │ │ │ ├── delete │ │ │ │ └── main.tf │ │ │ ├── create │ │ │ │ └── main.tf │ │ │ ├── update │ │ │ │ └── main.tf │ │ │ └── dynamic_resources.json │ │ ├── simple │ │ │ ├── delete │ │ │ │ └── main.tf │ │ │ ├── create │ │ │ │ └── main.tf │ │ │ └── update │ │ │ │ └── main.tf │ │ ├── multiple_dynamic_resources │ │ │ ├── delete │ │ │ │ └── main.tf │ │ │ ├── create │ │ │ │ └── main.tf │ │ │ └── dynamic_resources.json │ │ ├── fail_on │ │ │ ├── delete │ │ │ │ ├── delete.tf │ │ │ │ └── create.tf │ │ │ ├── read │ │ │ │ └── main.tf │ │ │ ├── create │ │ │ │ └── main.tf │ │ │ └── update │ │ │ │ ├── create.tf │ │ │ │ └── update.tf │ │ ├── list │ │ │ ├── simple │ │ │ │ ├── main.tfquery.hcl │ │ │ │ └── main.tf │ │ │ └── dynamic │ │ │ │ ├── main.tfquery.hcl │ │ │ │ ├── dynamic_resources.json │ │ │ │ └── main.tf │ │ ├── dynamic_datasource │ │ │ ├── delete │ │ │ │ └── main.tf │ │ │ ├── dynamic_resources.json │ │ │ └── create │ │ │ │ └── main.tf │ │ ├── dynamic_with_id │ │ │ ├── create │ │ │ │ └── main.tf │ │ │ └── update │ │ │ │ └── main.tf │ │ ├── simple_with_id │ │ │ ├── create │ │ │ │ └── main.tf │ │ │ └── update │ │ │ │ └── main.tf │ │ ├── deferral │ │ │ └── main.tf │ │ ├── deferral_unknown │ │ │ └── main.tf │ │ ├── dynamic_block │ │ │ ├── create │ │ │ │ └── main.tf │ │ │ ├── update │ │ │ │ └── main.tf │ │ │ └── dynamic_resources.json │ │ ├── actions │ │ │ ├── dynamic_resources.json │ │ │ ├── simple.tf │ │ │ └── dynamic.tf │ │ ├── simple_datasource │ │ │ └── get │ │ │ │ └── main.tf │ │ ├── dynamic_computed_block │ │ │ ├── create │ │ │ │ └── main.tf │ │ │ └── dynamic_resources.json │ │ ├── dynamic_computed_block_set │ │ │ ├── create │ │ │ │ └── main.tf │ │ │ └── dynamic_resources.json │ │ ├── complex_block │ │ │ ├── create │ │ │ │ └── main.tf │ │ │ └── update │ │ │ │ └── main.tf │ │ ├── dynamic_computed │ │ │ ├── create │ │ │ │ └── main.tf │ │ │ └── dynamic_resources.json │ │ ├── dynamic_requires_replace │ │ │ ├── create │ │ │ │ └── main.tf │ │ │ ├── update │ │ │ │ └── main.tf │ │ │ └── dynamic_resources.json │ │ ├── dynamic_nested │ │ │ ├── create │ │ │ │ └── main.tf │ │ │ └── dynamic_resources.json │ │ └── depends_on │ │ │ └── create │ │ │ └── main.tf │ ├── terraform.data │ │ └── simple_resource.json │ ├── action_test.go │ ├── provider_test.go │ └── list_resource_test.go ├── schema │ ├── types.go │ ├── simple │ │ └── simple.go │ ├── dynamic │ │ └── dynamic.go │ ├── schema.go │ ├── attribute.go │ ├── block.go │ ├── complex │ │ └── complex.go │ ├── datasource.go │ ├── action.go │ └── resource.go ├── client │ ├── client.go │ ├── state.go │ └── local.go ├── resource │ ├── action.go │ ├── data_source.go │ ├── list_resource.go │ └── resource.go ├── data │ ├── resource.go │ ├── resource_test.go │ └── value.go └── computed │ └── computed.go ├── examples ├── dynamic-resources │ ├── computed │ │ ├── main.tf │ │ └── dynamic_resources.json │ ├── computed_with_value │ │ ├── main.tf │ │ └── dynamic_resources.json │ ├── basic │ │ ├── main.tf │ │ └── dynamic_resources.json │ ├── basic-with-preset-id │ │ ├── main.tf │ │ └── dynamic_resources.json │ ├── with-complex-attributes │ │ ├── main.tf │ │ └── dynamic_resources.json │ ├── with-blocks │ │ ├── main.tf │ │ └── dynamic_resources.json │ ├── basic-with-data-source │ │ ├── terraform.data │ │ │ └── data_source.json │ │ ├── dynamic_resources.json │ │ └── main.tf │ ├── multiple-resources │ │ ├── main.tf │ │ └── dynamic_resources.json │ └── multiple-resources-with-data-source │ │ ├── terraform.data │ │ └── data_source.json │ │ ├── main.tf │ │ └── dynamic_resources.json ├── data-sources │ ├── tfcoremock_simple_resource │ │ ├── data-source.tf │ │ └── terraform.data │ │ │ └── my-simple-resource.json │ └── tfcoremock_complex_resource │ │ ├── data-source.tf │ │ └── terraform.data │ │ └── my-complex-resource.json ├── provider │ └── provider.tf ├── list-resources │ ├── tfcoremock_complex_resource │ │ └── list-resource.tf │ └── tfcoremock_simple_resource │ │ └── list-resource.tf ├── actions │ ├── tfcoremock_simple_resource │ │ └── action.tf │ └── tfcoremock_complex_resource │ │ └── action.tf ├── resources │ ├── tfcoremock_simple_resource │ │ └── resource.tf │ └── tfcoremock_complex_resource │ │ └── resource.tf └── README.md ├── terraform-registry-manifest.json ├── GNUmakefile ├── schema ├── schema.go └── dynamic_resources.json ├── tools └── tools.go ├── .release └── release-metadata.hcl ├── .copywrite.hcl ├── .gitignore ├── docs ├── list-resources │ ├── simple_resource.md │ └── complex_resource.md ├── data-sources │ └── simple_resource.md ├── actions │ └── simple_resource.md ├── resources │ └── simple_resource.md └── index.md ├── main.go ├── .goreleaser.yml ├── CHANGELOG.md ├── go.mod └── README.md /.go-version: -------------------------------------------------------------------------------- 1 | 1.24.1 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hashicorp/terraform-core 2 | -------------------------------------------------------------------------------- /internal/provider/testdata/complex/delete/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic/delete/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | -------------------------------------------------------------------------------- /internal/provider/testdata/simple/delete/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | -------------------------------------------------------------------------------- /internal/provider/testdata/multiple_dynamic_resources/delete/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | -------------------------------------------------------------------------------- /examples/dynamic-resources/computed/main.tf: -------------------------------------------------------------------------------- 1 | resource "tfcoremock_dynamic_resource" "example" {} 2 | -------------------------------------------------------------------------------- /examples/dynamic-resources/computed_with_value/main.tf: -------------------------------------------------------------------------------- 1 | resource "tfcoremock_dynamic_resource" "example" {} 2 | -------------------------------------------------------------------------------- /internal/provider/testdata/fail_on/delete/delete.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" { 2 | fail_on_delete = ["iden"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/dynamic-resources/basic/main.tf: -------------------------------------------------------------------------------- 1 | resource "tfcoremock_dynamic_resource" "example" { 2 | my_value = 0 3 | } 4 | -------------------------------------------------------------------------------- /terraform-registry-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "metadata": { 4 | "protocol_versions": ["6.0"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /internal/provider/testdata/list/simple/main.tfquery.hcl: -------------------------------------------------------------------------------- 1 | 2 | list "tfcoremock_simple_resource" "resource" { 3 | provider = tfcoremock 4 | } 5 | -------------------------------------------------------------------------------- /examples/data-sources/tfcoremock_simple_resource/data-source.tf: -------------------------------------------------------------------------------- 1 | data "tfcoremock_simple_resource" "example" { 2 | id = "my-simple-resource" 3 | } 4 | -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | default: testacc 2 | 3 | # Run acceptance tests 4 | .PHONY: testacc 5 | testacc: 6 | TF_ACC=1 go test ./... -v $(TESTARGS) -timeout 120m 7 | -------------------------------------------------------------------------------- /examples/data-sources/tfcoremock_complex_resource/data-source.tf: -------------------------------------------------------------------------------- 1 | data "tfcoremock_complex_resource" "example" { 2 | id = "my-complex-resource" 3 | } 4 | -------------------------------------------------------------------------------- /internal/provider/testdata/simple/create/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | resource "tfcoremock_simple_resource" "test" { 4 | integer = 0 5 | } 6 | -------------------------------------------------------------------------------- /internal/provider/testdata/simple/update/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | resource "tfcoremock_simple_resource" "test" { 4 | integer = 1 5 | } 6 | -------------------------------------------------------------------------------- /examples/dynamic-resources/basic-with-preset-id/main.tf: -------------------------------------------------------------------------------- 1 | resource "tfcoremock_dynamic_resource" "example" { 2 | id = "example" 3 | my_value = 0 4 | } 5 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic/create/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | resource "tfcoremock_dynamic_resource" "test" { 4 | integer = 0 5 | } 6 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic/update/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | resource "tfcoremock_dynamic_resource" "test" { 4 | integer = 1 5 | } 6 | -------------------------------------------------------------------------------- /examples/dynamic-resources/with-complex-attributes/main.tf: -------------------------------------------------------------------------------- 1 | resource "tfcoremock_dynamic_resource" "example" { 2 | my_values = [ 3 | "Hello, ", 4 | "World!" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /examples/provider/provider.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" { 2 | resource_directory = "terraform.resource" 3 | data_directory = "terraform.data" 4 | use_only_state = false 5 | } 6 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic_datasource/delete/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | data "tfcoremock_simple_resource" "simple_resource" { 4 | id = "simple_resource" 5 | } 6 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic_with_id/create/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | resource "tfcoremock_dynamic_resource" "test" { 4 | id = "my_id" 5 | integer = 0 6 | } 7 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic_with_id/update/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | resource "tfcoremock_dynamic_resource" "test" { 4 | id = "my_id" 5 | integer = 1 6 | } 7 | -------------------------------------------------------------------------------- /internal/provider/testdata/simple_with_id/create/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | resource "tfcoremock_simple_resource" "test" { 4 | id = "my_id" 5 | string = "hello" 6 | } 7 | -------------------------------------------------------------------------------- /internal/provider/testdata/simple_with_id/update/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | resource "tfcoremock_simple_resource" "test" { 4 | id = "my_id" 5 | string = "world" 6 | } 7 | -------------------------------------------------------------------------------- /internal/provider/testdata/fail_on/read/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" { 2 | fail_on_read = ["iden"] 3 | } 4 | 5 | resource "tfcoremock_simple_resource" "resource" { 6 | id = "iden" 7 | } 8 | -------------------------------------------------------------------------------- /examples/list-resources/tfcoremock_complex_resource/list-resource.tf: -------------------------------------------------------------------------------- 1 | list "tfcoremock_complex_resource" "example" { 2 | provider = tfcoremock 3 | config { 4 | id = "" # optional 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/list-resources/tfcoremock_simple_resource/list-resource.tf: -------------------------------------------------------------------------------- 1 | list "tfcoremock_simple_resource" "example" { 2 | provider = tfcoremock 3 | config { 4 | id = "" # optional 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /internal/provider/testdata/fail_on/create/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" { 2 | fail_on_create = ["iden"] 3 | } 4 | 5 | resource "tfcoremock_simple_resource" "resource" { 6 | id = "iden" 7 | } 8 | -------------------------------------------------------------------------------- /internal/provider/testdata/fail_on/delete/create.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" { 2 | fail_on_delete = ["iden"] 3 | } 4 | 5 | resource "tfcoremock_simple_resource" "resource" { 6 | id = "iden" 7 | } 8 | -------------------------------------------------------------------------------- /internal/provider/testdata/fail_on/update/create.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" { 2 | fail_on_update = ["iden"] 3 | } 4 | 5 | resource "tfcoremock_simple_resource" "resource" { 6 | id = "iden" 7 | } 8 | -------------------------------------------------------------------------------- /internal/provider/testdata/deferral/main.tf: -------------------------------------------------------------------------------- 1 | 2 | provider "tfcoremock" { 3 | defer_changes = ["defer_me"] 4 | } 5 | 6 | resource "tfcoremock_simple_resource" "resource" { 7 | id = "defer_me" 8 | } 9 | -------------------------------------------------------------------------------- /internal/provider/testdata/deferral_unknown/main.tf: -------------------------------------------------------------------------------- 1 | 2 | resource "tfcoremock_simple_resource" "main" {} 3 | 4 | resource "tfcoremock_simple_resource" "other" { 5 | id = tfcoremock_simple_resource.main.id 6 | } 7 | -------------------------------------------------------------------------------- /internal/provider/testdata/fail_on/update/update.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" { 2 | fail_on_update = ["iden"] 3 | } 4 | 5 | resource "tfcoremock_simple_resource" "resource" { 6 | id = "iden" 7 | number = 0 8 | } 9 | -------------------------------------------------------------------------------- /internal/provider/testdata/list/dynamic/main.tfquery.hcl: -------------------------------------------------------------------------------- 1 | 2 | list "tfcoremock_dynamic_resource" "resource" { 3 | provider = tfcoremock 4 | include_resource = true 5 | config { 6 | id = "one" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic_block/create/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | resource "tfcoremock_dynamic_resource" "test" { 4 | integer = 0 5 | 6 | nested_list { 7 | integer = 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/dynamic-resources/with-blocks/main.tf: -------------------------------------------------------------------------------- 1 | resource "tfcoremock_dynamic_resource" "example" { 2 | my_values { 3 | my_value = "Hello, " 4 | } 5 | 6 | my_values { 7 | my_value = "world!" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /schema/schema.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package schema 5 | 6 | import _ "embed" 7 | 8 | //go:embed dynamic_resources.json 9 | var DynamicResourcesJsonSchema string 10 | -------------------------------------------------------------------------------- /examples/dynamic-resources/basic-with-data-source/terraform.data/data_source.json: -------------------------------------------------------------------------------- 1 | { 2 | "values": { 3 | "integer": { 4 | "number": "0" 5 | }, 6 | "id": { 7 | "string": "data_source" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /internal/provider/testdata/list/simple/main.tf: -------------------------------------------------------------------------------- 1 | 2 | provider "tfcoremock" {} 3 | 4 | resource "tfcoremock_simple_resource" "one" { 5 | id = "one" 6 | } 7 | 8 | resource "tfcoremock_simple_resource" "two" { 9 | id = "two" 10 | } 11 | -------------------------------------------------------------------------------- /examples/dynamic-resources/basic/dynamic_resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "tfcoremock_dynamic_resource": { 3 | "attributes": { 4 | "my_value": { 5 | "type": "integer", 6 | "required": true 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/dynamic-resources/computed/dynamic_resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "tfcoremock_dynamic_resource": { 3 | "attributes": { 4 | "my_value": { 5 | "type": "string", 6 | "computed": true 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /internal/provider/testdata/actions/dynamic_resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "tfcoremock_dynamic_action": { 3 | "attributes": { 4 | "integer": { 5 | "type": "integer", 6 | "optional": true 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic/dynamic_resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "tfcoremock_dynamic_resource": { 3 | "attributes": { 4 | "integer": { 5 | "type": "integer", 6 | "optional": true 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /internal/provider/testdata/list/dynamic/dynamic_resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "tfcoremock_dynamic_resource": { 3 | "attributes": { 4 | "value": { 5 | "type": "string", 6 | "optional": true 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /internal/provider/testdata/multiple_dynamic_resources/create/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | resource "tfcoremock_integer" "integer" { 4 | integer = 404 5 | } 6 | 7 | resource "tfcoremock_string" "string" { 8 | string = "Hello, world!" 9 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /examples/actions/tfcoremock_simple_resource/action.tf: -------------------------------------------------------------------------------- 1 | action "tfcoremock_simple_resource" "example" { 2 | config { 3 | bool = true 4 | number = 0 5 | string = "Hello, world!" 6 | float = 0 7 | integer = 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/resources/tfcoremock_simple_resource/resource.tf: -------------------------------------------------------------------------------- 1 | resource "tfcoremock_simple_resource" "example" { 2 | id = "my-simple-resource" 3 | bool = true 4 | number = 0 5 | string = "Hello, world!" 6 | float = 0 7 | integer = 0 8 | } 9 | -------------------------------------------------------------------------------- /examples/dynamic-resources/multiple-resources/main.tf: -------------------------------------------------------------------------------- 1 | resource "tfcoremock_dynamic_resource_string_resource" "example" { 2 | my_value = "Hello, world!" 3 | } 4 | 5 | resource "tfcoremock_dynamic_resource_integer_resource" "example" { 6 | my_value = 0 7 | } 8 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic_datasource/dynamic_resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "tfcoremock_dynamic_resource": { 3 | "attributes": { 4 | "my_value": { 5 | "type": "integer", 6 | "required": true 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/dynamic-resources/basic-with-data-source/dynamic_resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "tfcoremock_dynamic_resource": { 3 | "attributes": { 4 | "my_value": { 5 | "type": "integer", 6 | "required": true 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/dynamic-resources/basic-with-data-source/main.tf: -------------------------------------------------------------------------------- 1 | data "tfcoremock_simple_resource" "example" { 2 | id = "data_source" 3 | } 4 | 5 | resource "tfcoremock_dynamic_resource" "example" { 6 | my_value = data.tfcoremock_simple_resource.example.integer 7 | } 8 | -------------------------------------------------------------------------------- /examples/dynamic-resources/basic-with-preset-id/dynamic_resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "tfcoremock_dynamic_resource": { 3 | "attributes": { 4 | "my_value": { 5 | "type": "integer", 6 | "required": true 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/dynamic-resources/multiple-resources-with-data-source/terraform.data/data_source.json: -------------------------------------------------------------------------------- 1 | { 2 | "values": { 3 | "id": { 4 | "string": "data_source" 5 | }, 6 | "my_value": { 7 | "string": "Hello, world!" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build tools 5 | 6 | package tools 7 | 8 | import ( 9 | // Documentation generation 10 | _ "github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs" 11 | ) 12 | -------------------------------------------------------------------------------- /internal/provider/terraform.data/simple_resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "values": { 3 | "id": { 4 | "string": "simple_resource" 5 | }, 6 | "integer": { 7 | "number": "0" 8 | }, 9 | "string": { 10 | "string": "data" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic_block/update/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | resource "tfcoremock_dynamic_resource" "test" { 4 | integer = 0 5 | 6 | nested_list { 7 | integer = 0 8 | } 9 | 10 | nested_list { 11 | integer = 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.release/release-metadata.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | url_source_repository = "https://github.com/hashicorp/terraform-provider-tfcoremock" 5 | url_license = "https://github.com/hashicorp/terraform-provider-tfcoremock/blob/main/LICENSE" 6 | -------------------------------------------------------------------------------- /internal/provider/testdata/simple_datasource/get/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | data "tfcoremock_simple_resource" "data" { 4 | id = "simple_resource" 5 | } 6 | 7 | resource "tfcoremock_simple_resource" "test" { 8 | integer = data.tfcoremock_simple_resource.data.integer 9 | } 10 | -------------------------------------------------------------------------------- /internal/provider/testdata/list/dynamic/main.tf: -------------------------------------------------------------------------------- 1 | 2 | provider "tfcoremock" {} 3 | 4 | resource "tfcoremock_dynamic_resource" "one" { 5 | id = "one" 6 | value = "hello, world" 7 | } 8 | 9 | resource "tfcoremock_dynamic_resource" "two" { 10 | id = "two" 11 | value = "goodbye, world" 12 | } 13 | -------------------------------------------------------------------------------- /examples/dynamic-resources/multiple-resources-with-data-source/main.tf: -------------------------------------------------------------------------------- 1 | data "tfcoremock_dynamic_resource_string_data_source" "example" { 2 | id = "data_source" 3 | } 4 | 5 | resource "tfcoremock_dynamic_resource_string_resource" "example" { 6 | my_value = data.mock_string_data_source.example.my_value 7 | } 8 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic_computed_block/create/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | resource "tfcoremock_dynamic_resource" "test" { 4 | 5 | other { 6 | id = "my-id" 7 | 8 | nested { 9 | 10 | } 11 | 12 | nested { 13 | 14 | } 15 | } 16 | 17 | other { 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic_computed_block_set/create/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | resource "tfcoremock_dynamic_resource" "test" { 4 | 5 | other { 6 | id = "my-id" 7 | 8 | nested { 9 | 10 | } 11 | 12 | nested { 13 | 14 | } 15 | } 16 | 17 | other { 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/dynamic-resources/with-complex-attributes/dynamic_resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "tfcoremock_dynamic_resource": { 3 | "attributes": { 4 | "my_values": { 5 | "type": "list", 6 | "required": true, 7 | "list": { 8 | "type": "string" 9 | } 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /internal/provider/testdata/complex_block/create/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | resource "tfcoremock_complex_resource" "test" { 4 | string = "hello" 5 | 6 | list_block { 7 | integer = 0 8 | } 9 | 10 | list_block { 11 | integer = 1 12 | } 13 | 14 | set_block { 15 | integer = 0 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/provider/testdata/complex_block/update/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | resource "tfcoremock_complex_resource" "test" { 4 | string = "hello" 5 | 6 | list_block { 7 | integer = 0 8 | } 9 | 10 | set_block { 11 | integer = 0 12 | } 13 | 14 | set_block { 15 | integer = 1 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic_datasource/create/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | data "tfcoremock_simple_resource" "test" { 4 | id = "simple_resource" 5 | } 6 | 7 | resource "tfcoremock_dynamic_resource" "test" { 8 | id = "my_dynamic_resource" 9 | my_value = data.tfcoremock_simple_resource.test.integer 10 | } 11 | -------------------------------------------------------------------------------- /internal/provider/testdata/actions/simple.tf: -------------------------------------------------------------------------------- 1 | 2 | resource "tfcoremock_simple_resource" "resource" { 3 | lifecycle { 4 | action_trigger { 5 | events = [before_create, before_update] 6 | actions = [action.tfcoremock_simple_resource.action] 7 | } 8 | } 9 | } 10 | 11 | action "tfcoremock_simple_resource" "action" {} 12 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic_computed/create/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | resource "tfcoremock_dynamic_resource" "test" { 4 | set = [ 5 | { 6 | "custom_value": "zero", 7 | }, 8 | { 9 | "custom_value": "one", 10 | }, 11 | { 12 | "custom_value": "two", 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic_requires_replace/create/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | resource "tfcoremock_list_of_objects" "test" { 4 | list = [ 5 | { 6 | key = "one" 7 | value = "first value" 8 | }, 9 | { 10 | key = "two" 11 | value = "second value" 12 | }, 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic_requires_replace/update/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | resource "tfcoremock_list_of_objects" "test" { 4 | list = [ 5 | { 6 | key = "three" 7 | value = "first value" 8 | }, 9 | { 10 | key = "two" 11 | value = "second value" 12 | }, 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/dynamic-resources/computed_with_value/dynamic_resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "tfcoremock_dynamic_resource": { 3 | "attributes": { 4 | "my_value": { 5 | "type": "string", 6 | "computed": true, 7 | "value": { 8 | "string": "my_generated_value" 9 | } 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/dynamic-resources/with-blocks/dynamic_resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "tfcoremock_dynamic_resource": { 3 | "blocks": { 4 | "my_values": { 5 | "mode": "list", 6 | "attributes": { 7 | "my_value": { 8 | "required": true, 9 | "type": "string" 10 | } 11 | } 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/testdata/actions/dynamic.tf: -------------------------------------------------------------------------------- 1 | 2 | resource "tfcoremock_simple_resource" "resource" { 3 | lifecycle { 4 | action_trigger { 5 | events = [before_create, before_update] 6 | actions = [action.tfcoremock_dynamic_action.action] 7 | } 8 | } 9 | } 10 | 11 | action "tfcoremock_dynamic_action" "action" { 12 | config { 13 | integer = 0 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic_nested/create/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | resource "tfcoremock_dynamic_resource" "test" { 4 | string = "hello" 5 | 6 | list = [ 7 | { 8 | string = "one" 9 | } 10 | ] 11 | 12 | metadata_free_list = [ 13 | { 14 | string = "other" 15 | } 16 | ] 17 | 18 | object = { 19 | bool = true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/provider/testdata/complex/create/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | resource "tfcoremock_complex_resource" "test" { 4 | string = "hello" 5 | 6 | list = [ 7 | { 8 | string = "one" 9 | } 10 | ] 11 | 12 | object = { 13 | bool = true 14 | } 15 | 16 | set = [ 17 | { 18 | string = "zero" 19 | }, 20 | { 21 | string = "one" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /internal/provider/testdata/complex/update/main.tf: -------------------------------------------------------------------------------- 1 | provider "tfcoremock" {} 2 | 3 | resource "tfcoremock_complex_resource" "test" { 4 | string = "hello" 5 | 6 | list = [ 7 | { 8 | string = "zero" 9 | }, 10 | { 11 | string = "one" 12 | } 13 | ] 14 | 15 | object = { 16 | string = "world" 17 | } 18 | 19 | set = [ 20 | { 21 | string = "zero" 22 | }, 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /internal/provider/testdata/multiple_dynamic_resources/dynamic_resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "tfcoremock_integer": { 3 | "attributes": { 4 | "integer": { 5 | "type": "integer", 6 | "required": true 7 | } 8 | } 9 | }, 10 | "tfcoremock_string": { 11 | "attributes": { 12 | "string": { 13 | "type": "string", 14 | "required": true 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /internal/schema/types.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package schema 5 | 6 | type Type string 7 | 8 | const ( 9 | Boolean Type = "boolean" 10 | Float Type = "float" 11 | Integer Type = "integer" 12 | Number Type = "number" 13 | String Type = "string" 14 | 15 | List Type = "list" 16 | Map Type = "map" 17 | Object Type = "object" 18 | Set Type = "set" 19 | ) 20 | -------------------------------------------------------------------------------- /.copywrite.hcl: -------------------------------------------------------------------------------- 1 | schema_version = 1 2 | 3 | project { 4 | license = "MPL-2.0" 5 | copyright_year = 2022 6 | 7 | # (OPTIONAL) A list of globs that should not have copyright/license headers. 8 | # Supports doublestar glob patterns for more flexibility in defining which 9 | # files or folders should be ignored 10 | header_ignore = [ 11 | "docs/**", 12 | "examples/**", 13 | "internal/provider/testdata/**", 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/testdata/depends_on/create/main.tf: -------------------------------------------------------------------------------- 1 | resource "tfcoremock_simple_resource" "example_one" { 2 | string = "resource_module1" 3 | } 4 | 5 | data "tfcoremock_simple_resource" "example_data" { 6 | id = "simple_resource" 7 | depends_on = [ 8 | tfcoremock_simple_resource.example_one 9 | ] 10 | } 11 | 12 | resource "tfcoremock_simple_resource" "example_two" { 13 | string = data.tfcoremock_simple_resource.example_data.string 14 | } 15 | -------------------------------------------------------------------------------- /examples/dynamic-resources/multiple-resources/dynamic_resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "tfcoremock_dynamic_resource_string_resource": { 3 | "attributes": { 4 | "my_value": { 5 | "type": "string", 6 | "required": true 7 | } 8 | } 9 | }, 10 | "tfcoremock_dynamic_resource_integer_resource": { 11 | "attributes": { 12 | "my_value": { 13 | "type": "integer", 14 | "required": true 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic_block/dynamic_resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "tfcoremock_dynamic_resource": { 3 | "attributes": { 4 | "integer": { 5 | "type": "integer", 6 | "optional": true 7 | } 8 | }, 9 | "blocks": { 10 | "nested_list": { 11 | "attributes": { 12 | "integer": { 13 | "type": "integer", 14 | "optional": true 15 | } 16 | } 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/data-sources/tfcoremock_simple_resource/terraform.data/my-simple-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "values": { 3 | "bool": { 4 | "boolean": true 5 | }, 6 | "float": { 7 | "number": "0" 8 | }, 9 | "id": { 10 | "string": "my-simple-resource" 11 | }, 12 | "integer": { 13 | "number": "0" 14 | }, 15 | "number": { 16 | "number": "0" 17 | }, 18 | "string": { 19 | "string": "Hello, world!" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/dynamic-resources/multiple-resources-with-data-source/dynamic_resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "tfcoremock_dynamic_resource_string_resource": { 3 | "attributes": { 4 | "my_value": { 5 | "type": "string", 6 | "required": true 7 | } 8 | } 9 | }, 10 | "tfcoremock_dynamic_resource_string_data_source": { 11 | "attributes": { 12 | "my_value": { 13 | "type": "string", 14 | "optional": true 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This directory contains examples that are mostly used for documentation, but can 4 | also be run/tested manually via the Terraform CLI. 5 | 6 | The examples held in the `data-sources`, `resources`, `actions`, and `providers` 7 | directories are used in the automatic documentation generation process. 8 | 9 | The examples held in the `dynamic-resources` directory are slightly more 10 | interesting and contain examples of generating different types of resources 11 | dynamically. 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 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 | 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.dll 2 | *.exe 3 | .DS_Store 4 | example.tf 5 | terraform.tfplan 6 | terraform.tfstate 7 | bin/ 8 | dist/ 9 | modules-dev/ 10 | /pkg/ 11 | website/.vagrant 12 | website/.bundle 13 | website/build 14 | website/node_modules 15 | .vagrant/ 16 | *.backup 17 | ./*.tfstate 18 | .terraform/ 19 | *.log 20 | *.bak 21 | *~ 22 | .*.swp 23 | .idea 24 | *.iml 25 | *.test 26 | *.iml 27 | 28 | website/vendor 29 | 30 | # Test exclusions 31 | !command/test-fixtures/**/*.tfstate 32 | !command/test-fixtures/**/.terraform/ 33 | 34 | # Keep windows files with windows line endings 35 | *.winfile eol=crlf 36 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic_requires_replace/dynamic_resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "tfcoremock_list_of_objects": { 3 | "attributes": { 4 | "list": { 5 | "required": true, 6 | "type": "list", 7 | "list": { 8 | "type": "object", 9 | "object": { 10 | "key": { 11 | "required": true, 12 | "type": "string", 13 | "replace": true 14 | }, 15 | "value": { 16 | "required": true, 17 | "type": "string" 18 | } 19 | } 20 | } 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic_computed_block_set/dynamic_resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "tfcoremock_dynamic_resource": { 3 | "blocks": { 4 | "other": { 5 | "mode": "set", 6 | "attributes": { 7 | "id": { 8 | "type": "string", 9 | "computed": true, 10 | "optional": true 11 | } 12 | }, 13 | "blocks": { 14 | "nested": { 15 | "attributes": { 16 | "id": { 17 | "type": "string", 18 | "computed": true 19 | } 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | We deeply appreciate any effort to discover and disclose security vulnerabilities responsibly. 2 | 3 | If you would like to report a vulnerability in one of our products, or have security concerns regarding HashiCorp software, please email [security@hashicorp.com](mailto:security@hashicorp.com). 4 | 5 | In order for us to best respond to your report, please include any of the following: 6 | 7 | * Steps to reproduce or proof-of-concept 8 | * Any relevant tools, including versions used 9 | * Tool output 10 | 11 | For additional information about HashiCorp security, please see [https://hashicorp.com/security](https://hashicorp.com/security). 12 | -------------------------------------------------------------------------------- /docs/list-resources/simple_resource.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "tfcoremock_simple_resource List Resource - terraform-provider-tfcoremock" 4 | subcategory: "" 5 | description: |- 6 | A simple resource that holds optional attributes for the five basic types: bool, number, string, float, and integer. 7 | --- 8 | 9 | # tfcoremock_simple_resource (List Resource) 10 | 11 | A simple resource that holds optional attributes for the five basic types: `bool`, `number`, `string`, `float`, and `integer`. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Read-Only 19 | 20 | - `id` (String) The ID of this resource. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # See GitHub's documentation for more information on this file: 2 | # https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates 3 | version: 2 4 | updates: 5 | - package-ecosystem: github-actions 6 | directory: / 7 | schedule: 8 | interval: monthly 9 | labels: 10 | - dependencies 11 | - automated 12 | - github_actions 13 | groups: 14 | github-actions-breaking: 15 | update-types: 16 | - major 17 | github-actions-backward-compatible: 18 | update-types: 19 | - minor 20 | - patch 21 | - package-ecosystem: "gomod" 22 | directory: "/" 23 | schedule: 24 | interval: "daily" 25 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic_computed_block/dynamic_resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "tfcoremock_dynamic_resource": { 3 | "blocks": { 4 | "object": { 5 | "attributes": { 6 | "id": { 7 | "type": "string", 8 | "computed": true 9 | } 10 | } 11 | }, 12 | "other": { 13 | "attributes": { 14 | "id": { 15 | "type": "string", 16 | "computed": true, 17 | "optional": true 18 | } 19 | }, 20 | "blocks": { 21 | "nested": { 22 | "attributes": { 23 | "id": { 24 | "type": "string", 25 | "computed": true 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /internal/client/client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package client 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/terraform-provider-tfcoremock/internal/data" 10 | ) 11 | 12 | func Filter(value string) *string { 13 | return &value 14 | } 15 | 16 | type Client interface { 17 | ReadResource(ctx context.Context, id string) (*data.Resource, error) 18 | WriteResource(ctx context.Context, value *data.Resource) error 19 | UpdateResource(ctx context.Context, value *data.Resource) error 20 | DeleteResource(ctx context.Context, id string) error 21 | ListResources(ctx context.Context, typeName *string, id *string, yield func(resource *data.Resource, err error), limit int64) error 22 | ReadDataSource(ctx context.Context, id string) (*data.Resource, error) 23 | } 24 | -------------------------------------------------------------------------------- /docs/data-sources/simple_resource.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "tfcoremock_simple_resource Data Source - terraform-provider-tfcoremock" 4 | subcategory: "" 5 | description: |- 6 | A simple resource that holds optional attributes for the five basic types: bool, number, string, float, and integer. 7 | --- 8 | 9 | # tfcoremock_simple_resource (Data Source) 10 | 11 | A simple resource that holds optional attributes for the five basic types: `bool`, `number`, `string`, `float`, and `integer`. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | data "tfcoremock_simple_resource" "example" { 17 | id = "my-simple-resource" 18 | } 19 | ``` 20 | 21 | 22 | ## Schema 23 | 24 | ### Read-Only 25 | 26 | - `bool` (Boolean) An optional boolean attribute, can be true or false. 27 | - `float` (Number) An optional float attribute. 28 | - `id` (String) The ID of this resource. 29 | - `integer` (Number) An optional integer attribute. 30 | - `number` (Number) An optional number attribute, can be an integer or a float. 31 | - `string` (String) An optional string attribute. 32 | -------------------------------------------------------------------------------- /examples/resources/tfcoremock_complex_resource/resource.tf: -------------------------------------------------------------------------------- 1 | resource "tfcoremock_complex_resource" "example" { 2 | id = "my-complex-resource" 3 | 4 | bool = true 5 | number = 0 6 | string = "Hello, world!" 7 | float = 0 8 | integer = 0 9 | 10 | list = [ 11 | { 12 | string = "list.one" 13 | }, 14 | { 15 | string = "list.two" 16 | } 17 | ] 18 | 19 | set = [ 20 | { 21 | string = "set.one" 22 | }, 23 | { 24 | string = "set.two" 25 | } 26 | ] 27 | 28 | map = { 29 | "one" : { 30 | string = "map.one" 31 | }, 32 | "two" : { 33 | string = "map.two" 34 | } 35 | } 36 | 37 | object = { 38 | 39 | string = "nested object" 40 | 41 | object = { 42 | string = "nested nested object" 43 | } 44 | } 45 | 46 | list_block { 47 | string = "list_block.one" 48 | } 49 | 50 | list_block { 51 | string = "list_block.two" 52 | } 53 | 54 | list_block { 55 | string = "list_block.three" 56 | } 57 | 58 | set_block { 59 | string = "set_block.one" 60 | } 61 | 62 | set_block { 63 | string = "set_block.two" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /docs/actions/simple_resource.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "tfcoremock_simple_resource Action - terraform-provider-tfcoremock" 4 | subcategory: "" 5 | description: |- 6 | A simple resource that holds optional attributes for the five basic types: bool, number, string, float, and integer. 7 | --- 8 | 9 | # tfcoremock_simple_resource (Action) 10 | 11 | A simple resource that holds optional attributes for the five basic types: `bool`, `number`, `string`, `float`, and `integer`. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | action "tfcoremock_simple_resource" "example" { 17 | config { 18 | bool = true 19 | number = 0 20 | string = "Hello, world!" 21 | float = 0 22 | integer = 0 23 | } 24 | } 25 | ``` 26 | 27 | 28 | ## Schema 29 | 30 | ### Optional 31 | 32 | - `bool` (Boolean) An optional boolean attribute, can be true or false. 33 | - `float` (Number) An optional float attribute. 34 | - `integer` (Number) An optional integer attribute. 35 | - `number` (Number) An optional number attribute, can be an integer or a float. 36 | - `string` (String) An optional string attribute. 37 | -------------------------------------------------------------------------------- /examples/actions/tfcoremock_complex_resource/action.tf: -------------------------------------------------------------------------------- 1 | action "tfcoremock_complex_resource" "example" { 2 | config { 3 | bool = true 4 | number = 0 5 | string = "Hello, world!" 6 | float = 0 7 | integer = 0 8 | 9 | list = [ 10 | { 11 | string = "list.one" 12 | }, 13 | { 14 | string = "list.two" 15 | } 16 | ] 17 | 18 | set = [ 19 | { 20 | string = "set.one" 21 | }, 22 | { 23 | string = "set.two" 24 | } 25 | ] 26 | 27 | map = { 28 | "one" : { 29 | string = "map.one" 30 | }, 31 | "two" : { 32 | string = "map.two" 33 | } 34 | } 35 | 36 | object = { 37 | 38 | string = "nested object" 39 | 40 | object = { 41 | string = "nested nested object" 42 | } 43 | } 44 | 45 | list_block { 46 | string = "list_block.one" 47 | } 48 | 49 | list_block { 50 | string = "list_block.two" 51 | } 52 | 53 | list_block { 54 | string = "list_block.three" 55 | } 56 | 57 | set_block { 58 | string = "set_block.one" 59 | } 60 | 61 | set_block { 62 | string = "set_block.two" 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /internal/provider/action_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/go-version" 7 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 8 | "github.com/hashicorp/terraform-plugin-testing/tfversion" 9 | ) 10 | 11 | func TestAccSimpleAction(t *testing.T) { 12 | t.Cleanup(CleanupTestingDirectories(t)) 13 | resource.Test(t, resource.TestCase{ 14 | ProtoV6ProviderFactories: ProviderFactories(""), 15 | TerraformVersionChecks: []tfversion.TerraformVersionCheck{ 16 | tfversion.SkipBelow(version.Must(version.NewVersion("1.14.0-beta1"))), 17 | }, 18 | Steps: []resource.TestStep{ 19 | { 20 | Config: LoadFile(t, "testdata/actions/simple.tf"), 21 | }, 22 | }, 23 | }) 24 | } 25 | 26 | func TestAccDynamicAction(t *testing.T) { 27 | t.Cleanup(CleanupTestingDirectories(t)) 28 | resource.Test(t, resource.TestCase{ 29 | ProtoV6ProviderFactories: ProviderFactories(LoadFile(t, "testdata/actions/dynamic_resources.json")), 30 | TerraformVersionChecks: []tfversion.TerraformVersionCheck{ 31 | tfversion.SkipBelow(version.Must(version.NewVersion("1.14.0-beta1"))), 32 | }, 33 | Steps: []resource.TestStep{ 34 | { 35 | Config: LoadFile(t, "testdata/actions/dynamic.tf"), 36 | }, 37 | }, 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /docs/resources/simple_resource.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "tfcoremock_simple_resource Resource - terraform-provider-tfcoremock" 4 | subcategory: "" 5 | description: |- 6 | A simple resource that holds optional attributes for the five basic types: bool, number, string, float, and integer. 7 | --- 8 | 9 | # tfcoremock_simple_resource (Resource) 10 | 11 | A simple resource that holds optional attributes for the five basic types: `bool`, `number`, `string`, `float`, and `integer`. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "tfcoremock_simple_resource" "example" { 17 | id = "my-simple-resource" 18 | bool = true 19 | number = 0 20 | string = "Hello, world!" 21 | float = 0 22 | integer = 0 23 | } 24 | ``` 25 | 26 | 27 | ## Schema 28 | 29 | ### Optional 30 | 31 | - `bool` (Boolean) An optional boolean attribute, can be true or false. 32 | - `float` (Number) An optional float attribute. 33 | - `integer` (Number) An optional integer attribute. 34 | - `number` (Number) An optional number attribute, can be an integer or a float. 35 | - `string` (String) An optional string attribute. 36 | 37 | ### Read-Only 38 | 39 | - `id` (String) The ID of this resource. 40 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic_nested/dynamic_resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "tfcoremock_dynamic_resource": { 3 | "attributes": { 4 | "string": { 5 | "type": "string", 6 | "optional": true 7 | }, 8 | "list": { 9 | "type": "list", 10 | "optional": true, 11 | "list": { 12 | "type": "object", 13 | "object": { 14 | "string": { 15 | "type": "string", 16 | "optional": true 17 | } 18 | } 19 | } 20 | }, 21 | "metadata_free_list": { 22 | "type": "list", 23 | "optional": true, 24 | "skip_nested_metadata": true, 25 | "list": { 26 | "type": "object", 27 | "object": { 28 | "string": { 29 | "type": "string" 30 | } 31 | } 32 | } 33 | }, 34 | "object": { 35 | "type": "object", 36 | "optional": true, 37 | "object": { 38 | "bool": { 39 | "type": "boolean", 40 | "optional": true 41 | } 42 | } 43 | }, 44 | "set": { 45 | "type": "set", 46 | "optional": true, 47 | "set": { 48 | "type": "object", 49 | "object": { 50 | "string": { 51 | "type": "string", 52 | "optional": true 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/client/state.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package client 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "fmt" 10 | "os" 11 | "path" 12 | 13 | "github.com/hashicorp/terraform-plugin-log/tflog" 14 | 15 | "github.com/hashicorp/terraform-provider-tfcoremock/internal/data" 16 | ) 17 | 18 | var _ Client = State{} 19 | 20 | type State struct { 21 | DataDirectory string 22 | } 23 | 24 | func (state State) ReadResource(ctx context.Context, id string) (*data.Resource, error) { 25 | return nil, nil 26 | } 27 | 28 | func (state State) WriteResource(ctx context.Context, value *data.Resource) error { 29 | return nil 30 | } 31 | 32 | func (state State) UpdateResource(ctx context.Context, value *data.Resource) error { 33 | return nil 34 | } 35 | 36 | func (state State) DeleteResource(ctx context.Context, id string) error { 37 | return nil 38 | } 39 | 40 | func (state State) ReadDataSource(ctx context.Context, id string) (*data.Resource, error) { 41 | tflog.Trace(ctx, "Local.ReadDataSource") 42 | 43 | jsonPath := path.Join(state.DataDirectory, fmt.Sprintf("%s.json", id)) 44 | 45 | jsonData, err := os.ReadFile(jsonPath) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | var value data.Resource 51 | if err := json.Unmarshal(jsonData, &value); err != nil { 52 | return nil, err 53 | } 54 | 55 | return &value, nil 56 | } 57 | 58 | func (state State) ListResources(ctx context.Context, typeName *string, id *string, yield func(resource *data.Resource, err error), limit int64) error { 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "flag" 9 | "log" 10 | 11 | "github.com/hashicorp/terraform-plugin-framework/providerserver" 12 | 13 | "github.com/hashicorp/terraform-provider-tfcoremock/internal/provider" 14 | ) 15 | 16 | // Run "go generate" to format example terraform files and generate the docs for the registry/website 17 | 18 | // If you do not have terraform installed, you can remove the formatting command, but its suggested to 19 | // ensure the documentation is formatted properly. 20 | //go:generate terraform fmt -recursive ./examples/ 21 | 22 | // Run the docs generation tool, check its repository for more information on how it works and how docs 23 | // can be customized. 24 | //go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs 25 | 26 | var ( 27 | // these will be set by the goreleaser configuration 28 | // to appropriate values for the compiled binary 29 | version string = "dev" 30 | 31 | // goreleaser can also pass the specific commit if you want 32 | // commit string = "" 33 | ) 34 | 35 | func main() { 36 | var debug bool 37 | 38 | flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve") 39 | flag.Parse() 40 | 41 | opts := providerserver.ServeOpts{ 42 | Address: "registry.terraform.io/hashicorp/tfcoremock", 43 | Debug: debug, 44 | } 45 | 46 | err := providerserver.Serve(context.Background(), provider.New(version), opts) 47 | 48 | if err != nil { 49 | log.Fatal(err.Error()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /internal/resource/action.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/hashicorp/terraform-plugin-framework/action" 9 | "github.com/hashicorp/terraform-plugin-framework/diag" 10 | 11 | "github.com/hashicorp/terraform-provider-tfcoremock/internal/data" 12 | "github.com/hashicorp/terraform-provider-tfcoremock/internal/schema" 13 | ) 14 | 15 | var _ action.Action = Action{} 16 | 17 | type Action struct { 18 | Name string 19 | InternalSchema schema.Schema 20 | } 21 | 22 | func (a Action) Metadata(ctx context.Context, request action.MetadataRequest, response *action.MetadataResponse) { 23 | response.TypeName = a.Name 24 | } 25 | 26 | func (a Action) Schema(ctx context.Context, request action.SchemaRequest, response *action.SchemaResponse) { 27 | var err error 28 | if response.Schema, err = a.InternalSchema.ToTerraformActionSchema(); err != nil { 29 | response.Diagnostics.Append(diag.NewErrorDiagnostic(fmt.Sprintf("failed to build resource schema for '%s'", a.Name), err.Error())) 30 | } 31 | } 32 | 33 | func (a Action) Invoke(ctx context.Context, request action.InvokeRequest, response *action.InvokeResponse) { 34 | resource := &data.Resource{} 35 | response.Diagnostics.Append(request.Config.Get(ctx, &resource)...) 36 | if response.Diagnostics.HasError() { 37 | return 38 | } 39 | 40 | msg, err := json.Marshal(resource) 41 | if err != nil { 42 | response.Diagnostics.AddError("failed to marshal action data", err.Error()) 43 | return 44 | } 45 | response.SendProgress(action.InvokeProgressEvent{ 46 | Message: string(msg), 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /internal/provider/testdata/dynamic_computed/dynamic_resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "tfcoremock_dynamic_resource": { 3 | "attributes": { 4 | "computed_list": { 5 | "type": "list", 6 | "computed": true, 7 | "list": { 8 | "type": "integer" 9 | } 10 | }, 11 | "computed_object": { 12 | "type": "object", 13 | "computed": true, 14 | "object": { 15 | "id": { 16 | "type": "string", 17 | "computed": true 18 | } 19 | } 20 | }, 21 | "set": { 22 | "type": "set", 23 | "required": true, 24 | "set": { 25 | "type": "object", 26 | "object": { 27 | "id": { 28 | "type": "string", 29 | "computed": true 30 | }, 31 | "optional_id": { 32 | "type": "string", 33 | "computed": true, 34 | "optional": true 35 | }, 36 | "custom_value": { 37 | "type": "string", 38 | "required": true 39 | } 40 | } 41 | } 42 | }, 43 | "object_with_value": { 44 | "type": "object", 45 | "computed": true, 46 | "object": { 47 | "boolean": { 48 | "type": "boolean", 49 | "optional": true 50 | }, 51 | "string": { 52 | "type": "string", 53 | "optional": true 54 | } 55 | }, 56 | "value": { 57 | "object": { 58 | "boolean": { 59 | "boolean": true 60 | }, 61 | "string": { 62 | "string": "hello" 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /internal/schema/simple/simple.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package simple 5 | 6 | import "github.com/hashicorp/terraform-provider-tfcoremock/internal/schema" 7 | 8 | var ( 9 | description = "A simple resource that holds optional attributes for the five basic types: bool, number, string, float and integer." 10 | markdownDescription = "A simple resource that holds optional attributes for the five basic types: `bool`, `number`, `string`, `float`, and `integer`." 11 | 12 | Schema = schema.Schema{ 13 | Description: description, 14 | MarkdownDescription: markdownDescription, 15 | Attributes: map[string]schema.Attribute{ 16 | "bool": { 17 | Description: "An optional boolean attribute, can be true or false.", 18 | MarkdownDescription: "An optional boolean attribute, can be true or false.", 19 | Optional: true, 20 | Type: schema.Boolean, 21 | }, 22 | "number": { 23 | Description: "An optional number attribute, can be an integer or a float.", 24 | MarkdownDescription: "An optional number attribute, can be an integer or a float.", 25 | Optional: true, 26 | Type: schema.Number, 27 | }, 28 | "string": { 29 | Description: "An optional string attribute.", 30 | MarkdownDescription: "An optional string attribute.", 31 | Optional: true, 32 | Type: schema.String, 33 | }, 34 | "float": { 35 | Description: "An optional float attribute.", 36 | MarkdownDescription: "An optional float attribute.", 37 | Optional: true, 38 | Type: schema.Float, 39 | }, 40 | "integer": { 41 | Description: "An optional integer attribute.", 42 | MarkdownDescription: "An optional integer attribute.", 43 | Optional: true, 44 | Type: schema.Integer, 45 | }, 46 | }, 47 | } 48 | ) 49 | -------------------------------------------------------------------------------- /internal/provider/provider_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package provider 5 | 6 | import ( 7 | "errors" 8 | "os" 9 | "testing" 10 | 11 | "github.com/hashicorp/terraform-plugin-framework/providerserver" 12 | "github.com/hashicorp/terraform-plugin-go/tfprotov6" 13 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 14 | "github.com/hashicorp/terraform-plugin-testing/terraform" 15 | ) 16 | 17 | func ProviderFactories(resources string) map[string]func() (tfprotov6.ProviderServer, error) { 18 | provider := NewForTesting("test", resources)() 19 | return map[string]func() (tfprotov6.ProviderServer, error){ 20 | "tfcoremock": providerserver.NewProtocol6WithError(provider), 21 | } 22 | } 23 | 24 | func LoadFile(t *testing.T, file string) string { 25 | data, err := os.ReadFile(file) 26 | if err != nil { 27 | t.Fatalf("could not read file %s: %v", file, err.Error()) 28 | } 29 | 30 | return string(data) 31 | } 32 | 33 | func CleanupTestingDirectories(t *testing.T) func() { 34 | return func() { 35 | _, err := os.ReadDir("terraform.resource") 36 | if err != nil { 37 | if os.IsNotExist(err) { 38 | return // Then it's fine. 39 | } 40 | 41 | t.Fatalf("could not read the resource directory for cleanup: %v", err) 42 | } 43 | t.Fatalf("test should have deleted the resource directory on completion") 44 | } 45 | } 46 | 47 | func SaveResourceId(name string, id *string) resource.TestCheckFunc { 48 | return func(state *terraform.State) error { 49 | module := state.RootModule() 50 | rs, ok := module.Resources[name] 51 | if !ok { 52 | return errors.New("missing resource " + name) 53 | } 54 | 55 | *id = rs.Primary.Attributes["id"] 56 | return nil 57 | } 58 | } 59 | 60 | func CheckResourceIdChanged(name string, id *string) resource.TestCheckFunc { 61 | return func(state *terraform.State) error { 62 | module := state.RootModule() 63 | rs, ok := module.Resources[name] 64 | if !ok { 65 | return errors.New("missing resource " + name) 66 | } 67 | 68 | if *id == rs.Primary.Attributes["id"] { 69 | return errors.New("id value for " + name + " has not changed") 70 | } 71 | return nil 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /internal/resource/data_source.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-framework/datasource" 11 | "github.com/hashicorp/terraform-plugin-framework/diag" 12 | "github.com/hashicorp/terraform-plugin-go/tftypes" 13 | 14 | "github.com/hashicorp/terraform-provider-tfcoremock/internal/client" 15 | "github.com/hashicorp/terraform-provider-tfcoremock/internal/data" 16 | "github.com/hashicorp/terraform-provider-tfcoremock/internal/schema" 17 | ) 18 | 19 | var _ datasource.DataSource = DataSource{} 20 | 21 | type DataSource struct { 22 | Name string 23 | InternalSchema schema.Schema 24 | Client client.Client 25 | } 26 | 27 | func (d DataSource) Metadata(ctx context.Context, request datasource.MetadataRequest, response *datasource.MetadataResponse) { 28 | response.TypeName = d.Name 29 | } 30 | 31 | func (d DataSource) Schema(ctx context.Context, request datasource.SchemaRequest, response *datasource.SchemaResponse) { 32 | var err error 33 | if response.Schema, err = d.InternalSchema.ToTerraformDataSourceSchema(); err != nil { 34 | response.Diagnostics.Append(diag.NewErrorDiagnostic(fmt.Sprintf("failed to build data source schema for '%s'", d.Name), err.Error())) 35 | } 36 | } 37 | 38 | func (d DataSource) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { 39 | resource := &data.Resource{ 40 | ResourceType: d.Name, 41 | } 42 | 43 | response.Diagnostics.Append(request.Config.Get(ctx, &resource)...) 44 | if response.Diagnostics.HasError() { 45 | return 46 | } 47 | 48 | data, err := d.Client.ReadDataSource(ctx, resource.GetId()) 49 | if err != nil { 50 | response.Diagnostics.AddError("failed to read data source", err.Error()) 51 | return 52 | } 53 | 54 | if data == nil { 55 | response.Diagnostics.AddError( 56 | "target data source does not exist", 57 | fmt.Sprintf("data source at %s could not be found in data directory", resource.GetId())) 58 | } 59 | 60 | typ := request.Config.Schema.Type().TerraformType(ctx) 61 | response.Diagnostics.Append(response.State.Set(ctx, data.WithType(typ.(tftypes.Object)))...) 62 | } 63 | -------------------------------------------------------------------------------- /internal/schema/dynamic/dynamic.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dynamic 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "os" 10 | "strings" 11 | 12 | "github.com/pkg/errors" 13 | "github.com/xeipuuv/gojsonschema" 14 | 15 | "github.com/hashicorp/terraform-provider-tfcoremock/internal/schema" 16 | jsonschema "github.com/hashicorp/terraform-provider-tfcoremock/schema" 17 | ) 18 | 19 | type Reader interface { 20 | Read() (map[string]schema.Schema, error) 21 | } 22 | 23 | type FileReader struct { 24 | File string 25 | } 26 | 27 | type StringReader struct { 28 | Data string 29 | } 30 | 31 | func (r FileReader) Read() (map[string]schema.Schema, error) { 32 | schemaLoader := gojsonschema.NewStringLoader(jsonschema.DynamicResourcesJsonSchema) 33 | 34 | data, err := os.ReadFile(r.File) 35 | if err != nil { 36 | // TODO(liamcervante): It's okay if there is no dynamic_resources.json 37 | // file, but if the user has set the environment variable changing the 38 | // location maybe we should complain about it? 39 | if os.IsNotExist(err) { 40 | return nil, nil 41 | } 42 | return nil, errors.Wrap(err, "failed to read dynamic resources file") 43 | } 44 | 45 | documentLoader := gojsonschema.NewStringLoader(string(data)) 46 | result, err := gojsonschema.Validate(schemaLoader, documentLoader) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | if result.Valid() { 52 | var dynamicResources map[string]schema.Schema 53 | if len(data) > 0 { 54 | if err := json.Unmarshal(data, &dynamicResources); err != nil { 55 | return nil, errors.Wrap(err, "failed to unmarshal dynamic resources json") 56 | } 57 | } 58 | 59 | return dynamicResources, nil 60 | } 61 | 62 | var errs []string 63 | for _, err := range result.Errors() { 64 | errs = append(errs, err.String()) 65 | } 66 | 67 | return nil, fmt.Errorf("failed json schema check: %s", strings.Join(errs, ", ")) 68 | } 69 | 70 | func (r StringReader) Read() (map[string]schema.Schema, error) { 71 | var dynamicResources map[string]schema.Schema 72 | if len(r.Data) > 0 { 73 | if err := json.Unmarshal([]byte(r.Data), &dynamicResources); err != nil { 74 | return nil, err 75 | } 76 | } 77 | return dynamicResources, nil 78 | } 79 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | go-version: 13 | runs-on: ubuntu-latest 14 | outputs: 15 | version: ${{ steps.go-version.outputs.version }} 16 | steps: 17 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 18 | - id: go-version 19 | run: echo "version=$(cat ./.go-version)" >> "$GITHUB_OUTPUT" 20 | release-notes: 21 | runs-on: ubuntu-latest 22 | env: 23 | RELEASE_VERSION: ${{ github.ref_name }} 24 | steps: 25 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 26 | - name: Generate Release Notes 27 | run: awk -v ver=${RELEASE_VERSION} '/^## / { if (p) { exit }; if ($2 == ver) { p=1; next } } p' CHANGELOG.md > release-notes.txt 28 | - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 29 | with: 30 | name: release-notes 31 | path: release-notes.txt 32 | retention-days: 1 33 | terraform-provider-release: 34 | name: 'Terraform Provider Release' 35 | needs: [go-version, release-notes] 36 | uses: hashicorp/ghaction-terraform-provider-release/.github/workflows/hashicorp.yml@5f388ae147bcc1e1c34822571b2f2de40694c5d6 # v5.0.0 37 | secrets: 38 | hc-releases-github-token: '${{ secrets.HASHI_RELEASES_GITHUB_TOKEN }}' 39 | hc-releases-host-prod: '${{ secrets.HC_RELEASES_HOST_PROD }}' 40 | hc-releases-host-staging: '${{ secrets.HC_RELEASES_HOST_STAGING }}' 41 | hc-releases-key-prod: '${{ secrets.HC_RELEASES_KEY_PROD }}' 42 | hc-releases-key-staging: '${{ secrets.HC_RELEASES_KEY_STAGING }}' 43 | hc-releases-terraform-registry-sync-token: '${{ secrets.TF_PROVIDER_RELEASE_TERRAFORM_REGISTRY_SYNC_TOKEN }}' 44 | setup-signore-github-token: '${{ secrets.HASHI_SIGNORE_GITHUB_TOKEN }}' 45 | signore-client-id: '${{ secrets.SIGNORE_CLIENT_ID }}' 46 | signore-client-secret: '${{ secrets.SIGNORE_CLIENT_SECRET }}' 47 | with: 48 | # Update to tag name if switched to branch-based workflow 49 | product-version: '${{ github.ref_name }}' 50 | release-notes: true 51 | setup-go-version: '${{ needs.go-version.outputs.version }}' 52 | -------------------------------------------------------------------------------- /docs/list-resources/complex_resource.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "tfcoremock_complex_resource List Resource - terraform-provider-tfcoremock" 4 | subcategory: "" 5 | description: |- 6 | A complex resource that contains five basic attributes, four complex attributes, and two nested blocks. 7 | The five basic attributes are boolean, number, string, float, and integer (as with the tfcoremock_simple_resource). 8 | The complex attributes are a map, a list, a set, and an object. The object type contains the same set of attributes as the schema itself, making a recursive structure. The list, set and map all contain objects which are also recursive. Blocks cannot go into attributes, so the complex attributes do not recurse on the block types. 9 | The blocks are a nested list_block and a nested set_block. The blocks contain the same set of attributes and blocks as the schema itself, also making a recursive structure. Note, blocks contain both attributes and more blocks so the block types are fully recursive. 10 | The complex and block types are nested 3 times, at the leaf level of recursion the complex attributes and blocks only contain the simple (ie. non-recursive) attributes. This prevents a potentially infinite level of recursion. 11 | --- 12 | 13 | # tfcoremock_complex_resource (List Resource) 14 | 15 | A complex resource that contains five basic attributes, four complex attributes, and two nested blocks. 16 | 17 | The five basic attributes are `boolean`, `number`, `string`, `float`, and `integer` (as with the `tfcoremock_simple_resource`). 18 | 19 | The complex attributes are a `map`, a `list`, a `set`, and an `object`. The `object` type contains the same set of attributes as the schema itself, making a recursive structure. The `list`, `set` and `map` all contain objects which are also recursive. Blocks cannot go into attributes, so the complex attributes do not recurse on the block types. 20 | 21 | The blocks are a nested `list_block` and a nested `set_block`. The blocks contain the same set of attributes and blocks as the schema itself, also making a recursive structure. Note, blocks contain both attributes and more blocks so the block types are fully recursive. 22 | 23 | The complex and block types are nested 3 times, at the leaf level of recursion the complex attributes and blocks only contain the simple (ie. non-recursive) attributes. This prevents a potentially infinite level of recursion. 24 | 25 | 26 | 27 | 28 | ## Schema 29 | 30 | ### Read-Only 31 | 32 | - `id` (String) The ID of this resource. 33 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | archives: 5 | - files: 6 | # Ensure only built binary and license file are archived 7 | - src: 'LICENSE' 8 | dst: 'LICENSE.txt' 9 | formats: [ 'zip' ] 10 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' 11 | before: 12 | hooks: 13 | - 'go mod download' 14 | builds: 15 | - # Binary naming only required for Terraform CLI 0.12 16 | binary: '{{ .ProjectName }}_v{{ .Version }}_x5' 17 | env: 18 | - CGO_ENABLED=0 19 | flags: 20 | - -trimpath 21 | goos: 22 | - darwin 23 | - freebsd 24 | - linux 25 | - windows 26 | goarch: 27 | - '386' 28 | - amd64 29 | - arm 30 | - arm64 31 | ignore: 32 | - goarch: arm 33 | goos: windows 34 | - goarch: arm64 35 | goos: freebsd 36 | - goarch: arm64 37 | goos: windows 38 | ldflags: 39 | - -s -w -X main.Version={{.Version}} 40 | mod_timestamp: '{{ .CommitTimestamp }}' 41 | checksum: 42 | algorithm: sha256 43 | extra_files: 44 | - glob: 'terraform-registry-manifest.json' 45 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' 46 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' 47 | publishers: 48 | - checksum: true 49 | # Terraform CLI 0.10 - 0.11 perform discovery via HTTP headers on releases.hashicorp.com 50 | # For providers which have existed since those CLI versions, exclude 51 | # discovery by setting the protocol version headers to 5. 52 | cmd: hc-releases upload -product={{ .ProjectName }} -version={{ .Version }} -file={{ .ArtifactPath }}={{ .ArtifactName }} -header=x-terraform-protocol-version=5 -header=x-terraform-protocol-versions=5.0 53 | env: 54 | - HC_RELEASES_HOST={{ .Env.HC_RELEASES_HOST }} 55 | - HC_RELEASES_KEY={{ .Env.HC_RELEASES_KEY }} 56 | extra_files: 57 | - glob: 'terraform-registry-manifest.json' 58 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' 59 | name: hc-releases upload 60 | signature: true 61 | release: 62 | extra_files: 63 | - glob: 'terraform-registry-manifest.json' 64 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' 65 | ids: 66 | - none 67 | signs: 68 | - args: ["sign", "--dearmor", "--file", "${artifact}", "--out", "${signature}"] 69 | artifacts: checksum 70 | cmd: signore 71 | signature: ${artifact}.sig 72 | - args: ["sign", "--dearmor", "--file", "${artifact}", "--out", "${signature}"] 73 | artifacts: checksum 74 | cmd: signore 75 | id: key-id 76 | signature: ${artifact}.72D7468F.sig 77 | snapshot: 78 | version_template: "{{ .Tag }}-next" 79 | -------------------------------------------------------------------------------- /internal/provider/list_resource_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/go-version" 7 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 8 | "github.com/hashicorp/terraform-plugin-testing/knownvalue" 9 | "github.com/hashicorp/terraform-plugin-testing/querycheck" 10 | "github.com/hashicorp/terraform-plugin-testing/querycheck/queryfilter" 11 | "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" 12 | "github.com/hashicorp/terraform-plugin-testing/tfversion" 13 | ) 14 | 15 | func TestAccSimpleResourceList(t *testing.T) { 16 | t.Cleanup(CleanupTestingDirectories(t)) 17 | resource.Test(t, resource.TestCase{ 18 | ProtoV6ProviderFactories: ProviderFactories(""), 19 | TerraformVersionChecks: []tfversion.TerraformVersionCheck{ 20 | tfversion.SkipBelow(version.Must(version.NewVersion("1.14.0"))), 21 | }, 22 | Steps: []resource.TestStep{ 23 | { 24 | Config: LoadFile(t, "testdata/list/simple/main.tf"), 25 | }, 26 | { 27 | Query: true, 28 | Config: LoadFile(t, "testdata/list/simple/main.tfquery.hcl"), 29 | QueryResultChecks: []querycheck.QueryResultCheck{ 30 | querycheck.ExpectIdentity("tfcoremock_simple_resource.resource", map[string]knownvalue.Check{ 31 | "id": knownvalue.StringExact("one"), 32 | }), 33 | querycheck.ExpectIdentity("tfcoremock_simple_resource.resource", map[string]knownvalue.Check{ 34 | "id": knownvalue.StringExact("two"), 35 | }), 36 | }, 37 | }, 38 | }, 39 | }) 40 | } 41 | 42 | func TestAccDynamicResourceList(t *testing.T) { 43 | t.Cleanup(CleanupTestingDirectories(t)) 44 | resource.Test(t, resource.TestCase{ 45 | ProtoV6ProviderFactories: ProviderFactories(LoadFile(t, "testdata/list/dynamic/dynamic_resources.json")), 46 | TerraformVersionChecks: []tfversion.TerraformVersionCheck{ 47 | tfversion.SkipBelow(version.Must(version.NewVersion("1.14.0"))), 48 | }, 49 | Steps: []resource.TestStep{ 50 | { 51 | Config: LoadFile(t, "testdata/list/dynamic/main.tf"), 52 | }, 53 | { 54 | Query: true, 55 | Config: LoadFile(t, "testdata/list/dynamic/main.tfquery.hcl"), 56 | QueryResultChecks: []querycheck.QueryResultCheck{ 57 | querycheck.ExpectIdentity("tfcoremock_dynamic_resource.resource", map[string]knownvalue.Check{ 58 | "id": knownvalue.StringExact("one"), 59 | }), 60 | querycheck.ExpectResourceKnownValues( 61 | "tfcoremock_dynamic_resource.resource", 62 | queryfilter.ByResourceIdentity(map[string]knownvalue.Check{ 63 | "id": knownvalue.StringExact("one"), 64 | }), 65 | []querycheck.KnownValueCheck{ 66 | { 67 | Path: tfjsonpath.New("value"), 68 | KnownValue: knownvalue.StringExact("hello, world"), 69 | }, 70 | }, 71 | ), 72 | }, 73 | }, 74 | }, 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /internal/resource/list_resource.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/terraform-plugin-framework/diag" 7 | "github.com/hashicorp/terraform-plugin-framework/list" 8 | list_schema "github.com/hashicorp/terraform-plugin-framework/list/schema" 9 | "github.com/hashicorp/terraform-plugin-framework/resource" 10 | "github.com/hashicorp/terraform-plugin-go/tftypes" 11 | 12 | "github.com/hashicorp/terraform-provider-tfcoremock/internal/client" 13 | "github.com/hashicorp/terraform-provider-tfcoremock/internal/data" 14 | "github.com/hashicorp/terraform-provider-tfcoremock/internal/schema" 15 | ) 16 | 17 | var _ list.ListResource = ListResource{} 18 | 19 | type ListResource struct { 20 | Name string 21 | InternalSchema schema.Schema 22 | Client client.Client 23 | } 24 | 25 | func (l ListResource) Metadata(ctx context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { 26 | response.TypeName = l.Name 27 | } 28 | 29 | func (l ListResource) ListResourceConfigSchema(ctx context.Context, request list.ListResourceSchemaRequest, response *list.ListResourceSchemaResponse) { 30 | response.Schema.Description = l.InternalSchema.Description 31 | response.Schema.MarkdownDescription = l.InternalSchema.MarkdownDescription 32 | response.Schema.Attributes = map[string]list_schema.Attribute{ 33 | "id": list_schema.StringAttribute{ 34 | Optional: true, 35 | }, 36 | } 37 | } 38 | 39 | func (l ListResource) List(ctx context.Context, request list.ListRequest, stream *list.ListResultsStream) { 40 | resource := &data.Resource{ 41 | ResourceType: l.Name, 42 | } 43 | 44 | diags := request.Config.Get(ctx, &resource) 45 | if diags.HasError() { 46 | stream.Results = list.ListResultsStreamDiagnostics(diags) 47 | return 48 | } 49 | 50 | stream.Results = func(yield func(list.ListResult) bool) { 51 | var id *string 52 | if value, ok := resource.Values["id"]; ok { 53 | id = value.String 54 | } 55 | 56 | err := l.Client.ListResources(ctx, client.Filter(l.Name), id, func(resource *data.Resource, err error) { 57 | result := request.NewListResult(ctx) 58 | if err != nil { 59 | result.Diagnostics.Append(diag.NewErrorDiagnostic("failed to query resource", err.Error())) 60 | return 61 | } else { 62 | result.DisplayName = resource.GetId() 63 | result.Diagnostics.Append(result.Identity.Set(ctx, resource.Identity())...) 64 | 65 | if request.IncludeResource { 66 | typ := request.ResourceSchema.Type().TerraformType(ctx) 67 | result.Diagnostics.Append(result.Resource.Set(ctx, resource.WithType(typ.(tftypes.Object)))...) 68 | } 69 | } 70 | yield(result) 71 | }, request.Limit) 72 | if err != nil { 73 | yield(list.ListResult{ 74 | Diagnostics: diag.Diagnostics{ 75 | diag.NewErrorDiagnostic("failed to query resources", err.Error()), 76 | }, 77 | }) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /schema/dynamic_resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://github.com/hashicorp/terraform-provider-tfcoremock/blob/main/schema/dynamic_resources.json", 4 | "title": "Dynamic Resources", 5 | "description": "The set of dynamic resources supported by the mock provider in the current working directory", 6 | "type": "object", 7 | "additionalProperties": { "$ref": "#/definitions/schema" }, 8 | "definitions": { 9 | "attribute": { 10 | "type": "object", 11 | "properties": { 12 | "type": { "type": "string" }, 13 | "optional": { "type": "boolean" }, 14 | "required": { "type": "boolean" }, 15 | "computed": { "type": "boolean" }, 16 | "sensitive": { "type": "boolean" }, 17 | "replace": { "type": "boolean" }, 18 | "skip_nested_metadata": { "type": "boolean" }, 19 | "value": { "$ref": "#/definitions/value" }, 20 | "list": { "$ref": "#/definitions/attribute" }, 21 | "map": { "$ref": "#/definitions/attribute" }, 22 | "object": { 23 | "type": "object", 24 | "additionalProperties": { "$ref": "#/definitions/attribute" } 25 | }, 26 | "set": { "$ref": "#/definitions/attribute" } 27 | }, 28 | "additionalProperties": false 29 | }, 30 | "block": { 31 | "type": "object", 32 | "properties": { 33 | "attributes": { 34 | "type": "object", 35 | "additionalProperties": { "$ref": "#/definitions/attribute" } 36 | }, 37 | "blocks": { 38 | "type": "object", 39 | "additionalProperties": { "$ref": "#/definitions/block" } 40 | }, 41 | "mode": { "type": "string" } 42 | }, 43 | "additionalProperties": false 44 | }, 45 | "schema": { 46 | "type": "object", 47 | "properties": { 48 | "attributes": { 49 | "type": "object", 50 | "additionalProperties": { "$ref": "#/definitions/attribute" } 51 | }, 52 | "blocks": { 53 | "type": "object", 54 | "additionalProperties": { "$ref": "#/definitions/block" } 55 | } 56 | }, 57 | "additionalProperties": false 58 | }, 59 | "value": { 60 | "type": "object", 61 | "properties": { 62 | "boolean": { "type": ["boolean", "null"] }, 63 | "number": { "type": ["string", "null"] }, 64 | "string": { "type": ["string", "null"] }, 65 | "list": { 66 | "type": ["array", "null"], 67 | "items": { "$ref": "#/definitions/value" } 68 | }, 69 | "map": { 70 | "type": ["object", "null"], 71 | "additionalProperties": { "$ref": "#/definitions/value" } 72 | }, 73 | "object": { 74 | "type": ["object", "null"], 75 | "additionalProperties": { "$ref": "#/definitions/value" } 76 | }, 77 | "set": { 78 | "type": ["array", "null"], 79 | "items": { "$ref": "#/definitions/value" } 80 | } 81 | }, 82 | "additionalProperties": false 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # Terraform Provider testing workflow. 2 | name: Tests 3 | 4 | # This GitHub action runs your tests for each pull request and push. 5 | # Optionally, you can turn it on using a schedule for regular testing. 6 | on: push 7 | 8 | # Testing only needs permissions to read the repository contents. 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | # Ensure project builds before running testing matrix 14 | build: 15 | name: Build 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 5 18 | steps: 19 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 20 | - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 21 | with: 22 | go-version-file: 'go.mod' 23 | cache: true 24 | - run: go mod download 25 | - run: go build -v . 26 | 27 | generate: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 31 | - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 32 | with: 33 | go-version-file: 'go.mod' 34 | cache: true 35 | - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 36 | with: 37 | terraform_version: 'v1.14.0-beta3' # TEMP, until actions are launched properly 38 | - run: go generate ./... 39 | - name: git diff 40 | run: | 41 | git diff --compact-summary --exit-code || \ 42 | (echo; echo "Unexpected difference in directories after code generation. Run 'go generate ./...' command and commit."; exit 1) 43 | 44 | # Run unit tests 45 | unit-test: 46 | name: Terraform Provider Unit Tests 47 | needs: build 48 | runs-on: ubuntu-latest 49 | timeout-minutes: 5 50 | steps: 51 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 52 | - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 53 | with: 54 | go-version-file: 'go.mod' 55 | cache: true 56 | - run: go test -v -cover ./... 57 | 58 | # Run acceptance tests in a matrix with Terraform CLI versions 59 | acceptance-test: 60 | name: Terraform Provider Acceptance Tests 61 | needs: build 62 | runs-on: ubuntu-latest 63 | timeout-minutes: 15 64 | strategy: 65 | fail-fast: false 66 | matrix: 67 | # list whatever Terraform versions here you would like to support 68 | terraform: 69 | - '1.0.*' 70 | - '1.1.*' 71 | - '1.2.*' 72 | - '1.3.*' 73 | - '1.4.*' 74 | - '1.5.*' 75 | - '1.6.*' 76 | - '1.7.*' 77 | - '1.8.*' 78 | - '1.9.*' 79 | - '1.10.*' 80 | - '1.11.*' 81 | - '1.12.*' 82 | - '1.13.*' 83 | # - '1.14.*' TODO: As soon as 1.14 is released. 84 | steps: 85 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 86 | - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 87 | with: 88 | go-version-file: 'go.mod' 89 | cache: true 90 | - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 91 | with: 92 | terraform_version: ${{ matrix.terraform }} 93 | terraform_wrapper: false 94 | - run: go mod download 95 | - env: 96 | TF_ACC: "1" 97 | run: go test -v -cover ./internal/provider/ 98 | timeout-minutes: 10 99 | -------------------------------------------------------------------------------- /internal/data/resource.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package data 5 | 6 | import ( 7 | "errors" 8 | 9 | "github.com/hashicorp/terraform-plugin-go/tftypes" 10 | ) 11 | 12 | var _ tftypes.ValueConverter = &Resource{} 13 | var _ tftypes.ValueCreator = &Resource{} 14 | 15 | // Resource is the data structure that is actually written into our data stores. 16 | // 17 | // It currently only publicly contains the Values mapping of attribute names to 18 | // actual values. It is designed as a bridge between the Terraform SDK 19 | // representation of a value and a generic JSON representation that can be 20 | // read/written externally. In theory, any terraform object can be represented 21 | // as a Resource. In practice, there will probably be edge cases and types that 22 | // have been missed. 23 | // 24 | // If we could write tftypes.Value into a human friendly format, and read back 25 | // any changes from that then we wouldn't need this bridge. But, we can't do 26 | // that using the current SDK so we handle it ourselves here. 27 | // 28 | // You must call the WithType function manually to attach the object type before 29 | // attempting to convert a Resource into a Terraform SDK value. 30 | // 31 | // The types are attached automatically when converting from a Terraform SDK 32 | // object. 33 | type Resource struct { 34 | ResourceType string `json:"resource_type"` 35 | Values map[string]Value `json:"values"` 36 | 37 | objectType tftypes.Object 38 | } 39 | 40 | // GetId returns the ID of the resource. 41 | // 42 | // It assumes the ID value exists and is a string type. 43 | func (r Resource) GetId() string { 44 | return *r.Values["id"].String 45 | } 46 | 47 | // Identity returns the identity type for this resource. 48 | func (r Resource) Identity() tftypes.Value { 49 | 50 | t := tftypes.Object{ 51 | AttributeTypes: map[string]tftypes.Type{ 52 | "id": tftypes.String, 53 | }, 54 | } 55 | 56 | return tftypes.NewValue(t, map[string]tftypes.Value{ 57 | "id": tftypes.NewValue(tftypes.String, r.GetId()), 58 | }) 59 | } 60 | 61 | // WithType adds type information into a Resource as this is not stored as part 62 | // of our external API. 63 | // 64 | // You must call this function to set the type information before using 65 | // ToTerraform5Value(). The type information can usually be retrieved from the 66 | // Terraform SDK, so this information should be readily available it just needs 67 | // to be added after the Resource has been created. 68 | func (r *Resource) WithType(objectType tftypes.Object) *Resource { 69 | r.objectType = objectType 70 | return r 71 | } 72 | 73 | // ToTerraform5Value ensures that Resource implements the tftypes.ValueCreator 74 | // interface, and so can be converted into Terraform types easily. 75 | func (r Resource) ToTerraform5Value() (interface{}, error) { 76 | return objectToTerraform5Value(&r.Values, r.objectType) 77 | } 78 | 79 | // FromTerraform5Value ensures that Resource implements the 80 | // tftypes.ValueConverter interface, and so can be converted from Terraform 81 | // types easily. 82 | func (r *Resource) FromTerraform5Value(value tftypes.Value) error { 83 | // It has to be an object we are converting from. 84 | if !value.Type().Is(tftypes.Object{}) { 85 | return errors.New("can only convert between object types") 86 | } 87 | 88 | values, err := FromTerraform5Value(value) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | // We know these kinds of conversions are safe now, as we checked the type 94 | // at the beginning. 95 | r.Values = *values.Object 96 | r.objectType = value.Type().(tftypes.Object) 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.6.0 (Unreleased) 2 | 3 | ENHANCEMENTS: 4 | 5 | * Add support for mocking actions. ([#191](https://github.com/hashicorp/terraform-provider-tfcoremock/pull/191)) 6 | * Add support for listing all resources. ([#193](https://github.com/hashicorp/terraform-provider-tfcoremock/pull/193)) 7 | * Introduce `defer_changes` attributes to the provider configuration. This allows controlling if resources should defer there changes during the current operation. ([#190](https://github.com/hashicorp/terraform-provider-tfcoremock/pull/190)) 8 | 9 | ## v0.5.0 (15 Apr 2025) 10 | 11 | NOTES: 12 | 13 | * Update dependencies. 14 | 15 | ## v0.4.0 (9 Jan 2025) 16 | 17 | ENHANCEMENTS: 18 | 19 | * Introduce `fail_on_create`, `fail_on_delete`, `fail_on_read`, `fail_on_update` attributes to the provider configuration. This allows controlling if resources should fail during certain operations. ([#154](https://github.com/hashicorp/terraform-provider-tfcoremock/pull/154)) 20 | 21 | ## v0.3.0 (26 Aug 2024) 22 | 23 | ENHANCEMENTS: 24 | 25 | * Destroying the last managed resource in a workspace will now cause the provider to also tidy up and remove the resource directory itself. ([#54](https://github.com/hashicorp/terraform-provider-tfcoremock/issues/54)) 26 | 27 | ## v0.2.0 (14 Apr 2023) 28 | 29 | ENHANCEMENTS: 30 | 31 | * Computed attributes in dynamic resources will no longer create default values, but will return null values by default. Users can still specify concrete values for computed attributes to return. ([#51](https://github.com/hashicorp/terraform-provider-tfcoremock/issues/51)) 32 | 33 | BUG FIXES: 34 | 35 | * Fix bug in which custom values for the resource and data directories were being interpreted incorrectly, meaning custom resource and data directories were unusable. ([#52](https://github.com/hashicorp/terraform-provider-tfcoremock/issues/52)) 36 | 37 | ## v0.1.3 (03 Apr 2023) 38 | 39 | BUG FIXES: 40 | 41 | * Fix bug in which data sources that were not setting the computed status for their attributes. ([#48](https://github.com/hashicorp/terraform-provider-tfcoremock/issues/48)) 42 | 43 | ## v0.1.2 (06 Dec 2022) 44 | 45 | FEATURES: 46 | 47 | * Introduce the `TFCOREMOCK_DYNAMIC_RESOURCES_FILE` environment variable. The location of the `dynamic_resources.json` file is now customisable. 48 | 49 | ## v0.1.1 (24 Nov 2022) 50 | 51 | FEATURES: 52 | 53 | * `sensitive`: Resource and data source attributes can be marked as sensitive, meaning they will be elided in Terraform plans and logs. 54 | * `replace`: Resource and data source attributes can be marked as forcing a replacement, meaning that when these attributes are modified the resource will be destroyed and recreated instead of just updated. 55 | * `skip_nested_metadata`: Resource and data source complex attributes can be created without embedded metadata. This doesn't change anything when editing Terraform config, but it changes the underlying format of the attributes and removes optional, sensitive, replacement metadata from attributes nested within complex attributes marked with this field. 56 | 57 | ## v0.1.0 (22 Nov 2022) 58 | 59 | First release of the Terraform Core Mock terraform provider. 60 | 61 | FEATURES: 62 | 63 | * `tfcoremock_simple_resource`: Resource and data source for a simple resource that can model numbers, strings, and booleans. 64 | * `tfcoremock_complex_resource`: Resource and data source for a complex resource that can model nested blocks, lists, sets, maps and objects. 65 | * Reads a `dynamic_resources.json` file to allow the user to specify additional resources and data sources dynamically. 66 | * Add support for computed attributes within dynamic resources. ([#5](https://github.com/hashicorp/terraform-provider-tfcoremock/pull/5)) 67 | -------------------------------------------------------------------------------- /examples/data-sources/tfcoremock_complex_resource/terraform.data/my-complex-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "values": { 3 | "bool": { 4 | "boolean": true 5 | }, 6 | "float": { 7 | "number": "0" 8 | }, 9 | "id": { 10 | "string": "my-complex-resource" 11 | }, 12 | "integer": { 13 | "number": "0" 14 | }, 15 | "list": { 16 | "list": [ 17 | { 18 | "object": { 19 | "string": { 20 | "string": "list.one" 21 | } 22 | } 23 | }, 24 | { 25 | "object": { 26 | "string": { 27 | "string": "list.two" 28 | } 29 | } 30 | } 31 | ] 32 | }, 33 | "list_block": { 34 | "list": [ 35 | { 36 | "object": { 37 | "list_block": { 38 | "list": [] 39 | }, 40 | "set_block": { 41 | "set": [] 42 | }, 43 | "string": { 44 | "string": "list_block.one" 45 | } 46 | } 47 | }, 48 | { 49 | "object": { 50 | "list_block": { 51 | "list": [] 52 | }, 53 | "set_block": { 54 | "set": [] 55 | }, 56 | "string": { 57 | "string": "list_block.two" 58 | } 59 | } 60 | }, 61 | { 62 | "object": { 63 | "list_block": { 64 | "list": [] 65 | }, 66 | "set_block": { 67 | "set": [] 68 | }, 69 | "string": { 70 | "string": "list_block.three" 71 | } 72 | } 73 | } 74 | ] 75 | }, 76 | "map": { 77 | "map": { 78 | "one": { 79 | "object": { 80 | "string": { 81 | "string": "map.one" 82 | } 83 | } 84 | }, 85 | "two": { 86 | "object": { 87 | "string": { 88 | "string": "map.two" 89 | } 90 | } 91 | } 92 | } 93 | }, 94 | "number": { 95 | "number": "0" 96 | }, 97 | "object": { 98 | "object": { 99 | "object": { 100 | "object": { 101 | "string": { 102 | "string": "nested nested object" 103 | } 104 | } 105 | }, 106 | "string": { 107 | "string": "nested object" 108 | } 109 | } 110 | }, 111 | "set": { 112 | "set": [ 113 | { 114 | "object": { 115 | "string": { 116 | "string": "set.one" 117 | } 118 | } 119 | }, 120 | { 121 | "object": { 122 | "string": { 123 | "string": "set.two" 124 | } 125 | } 126 | } 127 | ] 128 | }, 129 | "set_block": { 130 | "set": [ 131 | { 132 | "object": { 133 | "list_block": { 134 | "list": [] 135 | }, 136 | "set_block": { 137 | "set": [] 138 | }, 139 | "string": { 140 | "string": "set_block.one" 141 | } 142 | } 143 | }, 144 | { 145 | "object": { 146 | "list_block": { 147 | "list": [] 148 | }, 149 | "set_block": { 150 | "set": [] 151 | }, 152 | "string": { 153 | "string": "set_block.two" 154 | } 155 | } 156 | } 157 | ] 158 | }, 159 | "string": { 160 | "string": "Hello, world!" 161 | } 162 | } 163 | } -------------------------------------------------------------------------------- /internal/data/resource_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package data 5 | 6 | import ( 7 | "encoding/json" 8 | "math/big" 9 | "testing" 10 | 11 | "github.com/hashicorp/terraform-plugin-go/tftypes" 12 | ) 13 | 14 | func TestResource_symmetry(t *testing.T) { 15 | testCases := []struct { 16 | TestCase string 17 | Resource Resource 18 | }{ 19 | { 20 | TestCase: "basic", 21 | Resource: Resource{ 22 | objectType: tftypes.Object{ 23 | AttributeTypes: map[string]tftypes.Type{ 24 | "number": tftypes.Number, 25 | }, 26 | }, 27 | Values: map[string]Value{ 28 | "number": {Number: big.NewFloat(0)}, 29 | }, 30 | }, 31 | }, 32 | { 33 | TestCase: "missing_object", 34 | Resource: Resource{ 35 | objectType: tftypes.Object{ 36 | AttributeTypes: map[string]tftypes.Type{ 37 | "object": tftypes.Object{ 38 | AttributeTypes: map[string]tftypes.Type{ 39 | "number": tftypes.Number, 40 | }, 41 | }, 42 | }, 43 | }, 44 | Values: map[string]Value{}, 45 | }, 46 | }, 47 | { 48 | TestCase: "missing_object_attribute", 49 | Resource: Resource{ 50 | objectType: tftypes.Object{ 51 | AttributeTypes: map[string]tftypes.Type{ 52 | "object": tftypes.Object{ 53 | AttributeTypes: map[string]tftypes.Type{ 54 | "number": tftypes.Number, 55 | }, 56 | }, 57 | }, 58 | }, 59 | Values: map[string]Value{ 60 | "object": { 61 | Object: &map[string]Value{}, 62 | }, 63 | }, 64 | }, 65 | }, 66 | { 67 | TestCase: "missing_list", 68 | Resource: Resource{ 69 | objectType: tftypes.Object{ 70 | AttributeTypes: map[string]tftypes.Type{ 71 | "list": tftypes.List{ 72 | ElementType: tftypes.Number, 73 | }, 74 | }, 75 | }, 76 | Values: map[string]Value{}, 77 | }, 78 | }, 79 | { 80 | TestCase: "empty_list", 81 | Resource: Resource{ 82 | objectType: tftypes.Object{ 83 | AttributeTypes: map[string]tftypes.Type{ 84 | "list": tftypes.List{ 85 | ElementType: tftypes.Number, 86 | }, 87 | }, 88 | }, 89 | Values: map[string]Value{ 90 | "list": { 91 | List: &[]Value{}, 92 | }, 93 | }, 94 | }, 95 | }, 96 | { 97 | TestCase: "missing_map", 98 | Resource: Resource{ 99 | objectType: tftypes.Object{ 100 | AttributeTypes: map[string]tftypes.Type{ 101 | "map": tftypes.Map{ 102 | ElementType: tftypes.Number, 103 | }, 104 | }, 105 | }, 106 | Values: map[string]Value{}, 107 | }, 108 | }, 109 | { 110 | TestCase: "missing_set", 111 | Resource: Resource{ 112 | objectType: tftypes.Object{ 113 | AttributeTypes: map[string]tftypes.Type{ 114 | "set": tftypes.Set{ 115 | ElementType: tftypes.Number, 116 | }, 117 | }, 118 | }, 119 | Values: map[string]Value{}, 120 | }, 121 | }, 122 | } 123 | for _, testCase := range testCases { 124 | t.Run(testCase.TestCase, func(t *testing.T) { 125 | checkSymmetry(t, testCase.Resource) 126 | }) 127 | } 128 | } 129 | 130 | func toJson(t *testing.T, obj Resource) string { 131 | data, err := json.Marshal(obj) 132 | if err != nil { 133 | t.Fatalf("found unexpected error when marshalling json: %v", err) 134 | } 135 | return string(data) 136 | } 137 | 138 | func checkResourceEqual(t *testing.T, expected, actual Resource) { 139 | expectedString := toJson(t, expected) 140 | actualString := toJson(t, actual) 141 | if expectedString != actualString { 142 | t.Fatalf("expected did not match actual\nexpected:\n%s\nactual:\n%s", expectedString, actualString) 143 | } 144 | } 145 | 146 | func checkSymmetry(t *testing.T, resource Resource) { 147 | raw, err := resource.ToTerraform5Value() 148 | if err != nil { 149 | t.Fatalf("found unexpected error in ToTerraform5Value(): %v", err) 150 | } 151 | 152 | value := tftypes.NewValue(resource.objectType, raw) 153 | actual := Resource{} 154 | err = actual.FromTerraform5Value(value) 155 | if err != nil { 156 | t.Fatalf("found unexpected error in FromTerraform5Value(): %v", err) 157 | } 158 | 159 | checkResourceEqual(t, resource, actual) 160 | } 161 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/terraform-provider-tfcoremock 2 | 3 | go 1.24.1 4 | 5 | require ( 6 | github.com/hashicorp/go-uuid v1.0.3 7 | github.com/hashicorp/go-version v1.8.0 8 | github.com/hashicorp/terraform-plugin-docs v0.24.0 9 | github.com/hashicorp/terraform-plugin-framework v1.17.0 10 | github.com/hashicorp/terraform-plugin-go v0.29.0 11 | github.com/hashicorp/terraform-plugin-log v0.10.0 12 | github.com/hashicorp/terraform-plugin-testing v1.14.0 13 | github.com/pkg/errors v0.9.1 14 | github.com/xeipuuv/gojsonschema v1.2.0 15 | ) 16 | 17 | require ( 18 | github.com/BurntSushi/toml v1.2.1 // indirect 19 | github.com/Kunde21/markdownfmt/v3 v3.1.0 // indirect 20 | github.com/Masterminds/goutils v1.1.1 // indirect 21 | github.com/Masterminds/semver/v3 v3.2.0 // indirect 22 | github.com/Masterminds/sprig/v3 v3.2.3 // indirect 23 | github.com/ProtonMail/go-crypto v1.1.6 // indirect 24 | github.com/agext/levenshtein v1.2.2 // indirect 25 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 26 | github.com/armon/go-radix v1.0.0 // indirect 27 | github.com/bgentry/speakeasy v0.1.0 // indirect 28 | github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect 29 | github.com/cloudflare/circl v1.6.1 // indirect 30 | github.com/fatih/color v1.16.0 // indirect 31 | github.com/golang/protobuf v1.5.4 // indirect 32 | github.com/google/go-cmp v0.7.0 // indirect 33 | github.com/google/uuid v1.6.0 // indirect 34 | github.com/hashicorp/cli v1.1.7 // indirect 35 | github.com/hashicorp/errwrap v1.1.0 // indirect 36 | github.com/hashicorp/go-checkpoint v0.5.0 // indirect 37 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 38 | github.com/hashicorp/go-cty v1.5.0 // indirect 39 | github.com/hashicorp/go-hclog v1.6.3 // indirect 40 | github.com/hashicorp/go-multierror v1.1.1 // indirect 41 | github.com/hashicorp/go-plugin v1.7.0 // indirect 42 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 43 | github.com/hashicorp/hc-install v0.9.2 // indirect 44 | github.com/hashicorp/hcl/v2 v2.24.0 // indirect 45 | github.com/hashicorp/logutils v1.0.0 // indirect 46 | github.com/hashicorp/terraform-exec v0.24.0 // indirect 47 | github.com/hashicorp/terraform-json v0.27.2 // indirect 48 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 // indirect 49 | github.com/hashicorp/terraform-registry-address v0.4.0 // indirect 50 | github.com/hashicorp/terraform-svchost v0.1.1 // indirect 51 | github.com/hashicorp/yamux v0.1.2 // indirect 52 | github.com/huandu/xstrings v1.3.3 // indirect 53 | github.com/imdario/mergo v0.3.15 // indirect 54 | github.com/mattn/go-colorable v0.1.14 // indirect 55 | github.com/mattn/go-isatty v0.0.20 // indirect 56 | github.com/mattn/go-runewidth v0.0.9 // indirect 57 | github.com/mitchellh/copystructure v1.2.0 // indirect 58 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 59 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 60 | github.com/mitchellh/mapstructure v1.5.0 // indirect 61 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 62 | github.com/oklog/run v1.1.0 // indirect 63 | github.com/posener/complete v1.2.3 // indirect 64 | github.com/shopspring/decimal v1.3.1 // indirect 65 | github.com/spf13/cast v1.5.0 // indirect 66 | github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect 67 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 68 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 69 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 70 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 71 | github.com/yuin/goldmark v1.7.7 // indirect 72 | github.com/yuin/goldmark-meta v1.1.0 // indirect 73 | github.com/zclconf/go-cty v1.17.0 // indirect 74 | go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect 75 | golang.org/x/crypto v0.45.0 // indirect 76 | golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect 77 | golang.org/x/mod v0.29.0 // indirect 78 | golang.org/x/net v0.47.0 // indirect 79 | golang.org/x/sync v0.18.0 // indirect 80 | golang.org/x/sys v0.38.0 // indirect 81 | golang.org/x/text v0.31.0 // indirect 82 | golang.org/x/tools v0.38.0 // indirect 83 | google.golang.org/appengine v1.6.8 // indirect 84 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect 85 | google.golang.org/grpc v1.75.1 // indirect 86 | google.golang.org/protobuf v1.36.9 // indirect 87 | gopkg.in/yaml.v2 v2.3.0 // indirect 88 | gopkg.in/yaml.v3 v3.0.1 // indirect 89 | ) 90 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.MD: -------------------------------------------------------------------------------- 1 | # Contributing to `terraform-provider-tfcoremock` 2 | 3 | **First:** if you're unsure or afraid of _anything_, just ask or submit the issue describing the problem you're aiming to solve. 4 | 5 | To provide some safety to the Terraform ecosystem, we strictly follow [semantic versioning](https://semver.org/) and any changes that could be considered as breaking will only be released as part of a major release. 6 | 7 | ## Table of Contents 8 | 9 | - [I just have a question](#i-just-have-a-question) 10 | - [I want to report a vulnerability](#i-want-to-report-a-vulnerability) 11 | - [New Issue](#new-issue) 12 | - [New Pull Request](#new-pull-request) 13 | 14 | ## I just have a question 15 | 16 | > **Note:** We use GitHub for tracking bugs and feature requests only. 17 | 18 | For questions, please see relevant channels at https://www.terraform.io/community.html 19 | 20 | ## I want to report a vulnerability 21 | 22 | Please disclose security vulnerabilities responsibly by following the procedure described at https://www.hashicorp.com/security#vulnerability-reporting 23 | 24 | ## New Issue 25 | 26 | We welcome issues of all kinds including feature requests, bug reports or documentation suggestions. Below are guidelines for well-formed issues of each type. 27 | 28 | ### Bug Reports 29 | 30 | - **Test against latest release**: Make sure you test against the latest avaiable version of both Terraform and the provider. 31 | It is possible we already fixed the bug you're experiencing. 32 | 33 | - **Search for duplicates**: It's helpful to keep bug reports consolidated to one thread, so do a quick search on existing bug reports to check if anybody else has reported the same thing. You can scope searches by the label `bug` to help narrow things down. 34 | 35 | - **Include steps to reproduce**: Provide steps to reproduce the issue, along with code examples (both HCL and Go, where applicable) and/or real code, so we can try to reproduce it. Without this, it makes it much harder (sometimes impossible) to fix the issue. 36 | 37 | ### Feature Requests 38 | 39 | - **Search for possible duplicate requests**: It's helpful to keep requests consolidated to one thread, so do a quick search on existing requests to check if anybody else has reported the same thing. You can scope searches by the label `enhancement` to help narrow things down. 40 | 41 | - **Include a use case description**: In addition to describing the behavior of the feature you'd like to see added, it's helpful to also lay out the reason why the feature would be important and how it would benefit the wider Terraform ecosystem. Use case in context of 1 provider is good, wider context of more providers is better. 42 | 43 | ## New Pull Request 44 | 45 | Thank you for contributing! 46 | 47 | We are happy to review pull requests without associated issues, but we highly recommend starting by describing and discussing your problem or feature and attaching use cases to an issue first before raising a pull request. 48 | 49 | - **Early validation of idea and implementation plan**: Terraform's SDK is complicated enough that there are often several ways to implement something, each of which has different implications and tradeoffs. Working through a plan of attack with the team before you dive into implementation will help ensure that you're working in the right direction. 50 | 51 | - **Acceptance Tests**: It may go without saying, but every new patch should be covered by tests wherever possible. 52 | 53 | - **Go Modules**: We use [Go Modules](https://github.com/golang/go/wiki/Modules) to manage and version all our dependencies. Please make sure that you reflect dependency changes in your pull requests appropriately (e.g. `go get`, `go mod tidy` or other commands). Where possible it is better to raise a separate pull request with just dependency changes as it's easier to review such PR(s). 54 | 55 | ### Cosmetic changes, code formatting, and typos 56 | 57 | In general, we do not accept PRs containing only the following changes: 58 | 59 | - Correcting spelling or typos 60 | - Code formatting, including whitespace 61 | - Other cosmetic changes that do not affect functionality 62 | 63 | While we appreciate the effort that goes into preparing PRs, there is always a tradeoff between benefit and cost. The costs involved in accepting such contributions include the time taken for thorough review, the noise created in the git history, and the increased number of GitHub notifications that maintainers must attend to. 64 | 65 | #### Exceptions 66 | 67 | We believe that one should "leave the campsite cleaner than you found it", so you are welcome to clean up cosmetic issues in the neighbourhood when submitting a patch that makes functional changes or fixes. 68 | -------------------------------------------------------------------------------- /internal/schema/schema.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package schema 5 | 6 | import ( 7 | "errors" 8 | 9 | action_schema "github.com/hashicorp/terraform-plugin-framework/action/schema" 10 | datasource_schema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" 11 | resource_schema "github.com/hashicorp/terraform-plugin-framework/resource/schema" 12 | resource_schema_planmodifier "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 13 | resource_schema_stringplanmodifier "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 14 | ) 15 | 16 | // Schema defines an internal representation of a Terraform schema. 17 | // 18 | // It is designed to be read dynamically from a JSON object, allowing schemas, 19 | // blocks and attributes to be defined dynamically by the user of the provider. 20 | type Schema struct { 21 | Description string `json:"-"` // Dynamic resources don't need descriptions so hide them from the exposed JSON schema. 22 | MarkdownDescription string `json:"-"` // Dynamic resources don't need descriptions so hide them from the exposed JSON schema. 23 | Attributes map[string]Attribute `json:"attributes"` 24 | Blocks map[string]Block `json:"blocks"` 25 | } 26 | 27 | // AllAttributes returns the attributes for the dynamic schema, plus the 28 | // required ID attribute that is attached to tfsdk.Schema objects automatically. 29 | func (schema Schema) AllAttributes() map[string]Attribute { 30 | attributes := make(map[string]Attribute, 0) 31 | for key, attribute := range schema.Attributes { 32 | attributes[key] = attribute 33 | } 34 | attributes["id"] = Attribute{ 35 | Type: String, 36 | Optional: false, 37 | Required: false, 38 | Computed: true, 39 | } 40 | return attributes 41 | } 42 | 43 | // ToTerraformResourceSchema converts out representation of a Schema into a 44 | // Terraform SDK tfsdk.Schema. It automatically creates and attaches a computed 45 | // type called `id` that is required by every resource and data source in this 46 | // provider. 47 | func (schema Schema) ToTerraformResourceSchema() (resource_schema.Schema, error) { 48 | out := resource_schema.Schema{ 49 | Description: schema.Description, 50 | MarkdownDescription: schema.MarkdownDescription, 51 | } 52 | 53 | var err error 54 | if err = schema.validateAttributes(); err != nil { 55 | return out, err 56 | } 57 | 58 | if out.Attributes, err = attributesToTerraformResourceAttributes(schema.Attributes); err != nil { 59 | return out, err 60 | } 61 | out.Attributes["id"] = resource_schema.StringAttribute{ 62 | Required: false, 63 | Optional: true, 64 | Computed: true, 65 | PlanModifiers: []resource_schema_planmodifier.String{ 66 | resource_schema_stringplanmodifier.UseStateForUnknown(), 67 | resource_schema_stringplanmodifier.RequiresReplace(), 68 | }, 69 | } 70 | 71 | if out.Blocks, err = blocksToTerraformResourceBlocks(schema.Blocks); err != nil { 72 | return out, err 73 | } 74 | 75 | return out, nil 76 | } 77 | 78 | // ToTerraformDataSourceSchema converts our representation of a Schema into a 79 | // Terraform SDK tfsdk.Schema. It automatically creates and attaches a required 80 | // attribute called `id` that is required by every resource and data source in 81 | // this provider. 82 | func (schema Schema) ToTerraformDataSourceSchema() (datasource_schema.Schema, error) { 83 | out := datasource_schema.Schema{ 84 | Description: schema.Description, 85 | MarkdownDescription: schema.MarkdownDescription, 86 | } 87 | 88 | var err error 89 | if err = schema.validateAttributes(); err != nil { 90 | return out, err 91 | } 92 | 93 | if out.Attributes, err = attributesToTerraformDataSourceAttributes(schema.Attributes); err != nil { 94 | return out, err 95 | } 96 | 97 | out.Attributes["id"] = datasource_schema.StringAttribute{ 98 | Required: true, 99 | Optional: false, 100 | Computed: false, 101 | } 102 | 103 | if out.Blocks, err = blocksToTerraformDataSourceBlocks(schema.Blocks); err != nil { 104 | return out, err 105 | } 106 | 107 | return out, nil 108 | } 109 | 110 | func (schema Schema) ToTerraformActionSchema() (action_schema.Schema, error) { 111 | out := action_schema.Schema{ 112 | Description: schema.Description, 113 | MarkdownDescription: schema.MarkdownDescription, 114 | } 115 | 116 | var err error 117 | if err = schema.validateAttributes(); err != nil { 118 | return out, err 119 | } 120 | 121 | if out.Attributes, err = attributesToTerraformActionAttributes(schema.Attributes); err != nil { 122 | return out, err 123 | } 124 | 125 | if out.Blocks, err = blocksToTerraformActionBlocks(schema.Blocks); err != nil { 126 | return out, err 127 | } 128 | 129 | return out, nil 130 | } 131 | 132 | func (schema Schema) validateAttributes() error { 133 | if _, ok := schema.Attributes["id"]; ok { 134 | return errors.New("top level dynamic objects cannot define a value called `id` as the provider will generate an identifier for them") 135 | } 136 | return nil 137 | } 138 | -------------------------------------------------------------------------------- /internal/computed/computed.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package computed 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "github.com/hashicorp/terraform-provider-tfcoremock/internal/data" 10 | "github.com/hashicorp/terraform-provider-tfcoremock/internal/schema" 11 | ) 12 | 13 | // GenerateComputedValues steps through the resource and uses the schema to 14 | // populate any computed values. 15 | // 16 | // Computed values have a sensible default for all primitive types, and can be 17 | // specified using a data.Value object as part of the dynamic schema. 18 | // 19 | // Objects are complicated as you can have nested objects with required values 20 | // so the default value for a computed object is to generate an object with all 21 | // the required and computed values populated using a default. 22 | func GenerateComputedValues(resource *data.Resource, schema schema.Schema) error { 23 | if err := generateComputedValuesForObject(&resource.Values, schema.AllAttributes()); err != nil { 24 | return err 25 | } 26 | 27 | if err := generateComputedValuesForBlocks(&resource.Values, schema.Blocks); err != nil { 28 | return err 29 | } 30 | 31 | return nil 32 | } 33 | 34 | func generateComputedValuesForBlocks(values *map[string]data.Value, blocks map[string]schema.Block) error { 35 | for key, block := range blocks { 36 | var err error 37 | switch block.Mode { 38 | case schema.NestingModeSet: 39 | err = generateComputedValuesForBlock((*values)[key].Set, block) 40 | case "", schema.NestingModeList: 41 | err = generateComputedValuesForBlock((*values)[key].List, block) 42 | default: 43 | return errors.New("unrecognized block type: " + block.Mode) 44 | } 45 | 46 | if err != nil { 47 | return err 48 | } 49 | } 50 | return nil 51 | } 52 | 53 | func generateComputedValuesForBlock(values *[]data.Value, block schema.Block) error { 54 | if values == nil { 55 | return nil 56 | } 57 | 58 | for ix, value := range *values { 59 | if err := generateComputedValuesForObject(value.Object, block.Attributes); err != nil { 60 | return err 61 | } 62 | 63 | if err := generateComputedValuesForBlocks(value.Object, block.Blocks); err != nil { 64 | return err 65 | } 66 | 67 | (*values)[ix] = value 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func generateComputedValue(value data.Value, attribute *schema.Attribute) (data.Value, error) { 74 | var err error 75 | switch attribute.Type { 76 | case schema.Boolean, schema.Float, schema.Integer, schema.Number, schema.String: 77 | // For these types we don't need to do anything, they have a value 78 | // set and we're all good to leave them as is. 79 | case schema.List: 80 | err = generateComputedValuesForList(value.List, attribute.List) 81 | case schema.Set: 82 | err = generateComputedValuesForSet(value.Set, attribute.Set) 83 | case schema.Map: 84 | err = generateComputedValuesForMap(value.Map, attribute.Map) 85 | case schema.Object: 86 | err = generateComputedValuesForObject(value.Object, attribute.Object) 87 | default: 88 | return value, errors.New("unrecognized attribute type: " + string(attribute.Type)) 89 | } 90 | 91 | return value, err 92 | } 93 | 94 | func generateComputedValuesForList(values *[]data.Value, attribute *schema.Attribute) error { 95 | for ix, value := range *values { 96 | // Then we're going to go through each value and check if it has any 97 | // attributes that need to be computed. 98 | newValue, err := generateComputedValue(value, attribute) 99 | if err != nil { 100 | return err 101 | } 102 | (*values)[ix] = newValue 103 | } 104 | return nil 105 | } 106 | 107 | func generateComputedValuesForSet(values *[]data.Value, attribute *schema.Attribute) error { 108 | for ix, value := range *values { 109 | // Then we're going to go through each value and check if it has any 110 | // attributes that need to be computed. 111 | newValue, err := generateComputedValue(value, attribute) 112 | if err != nil { 113 | return err 114 | } 115 | (*values)[ix] = newValue 116 | } 117 | return nil 118 | } 119 | 120 | func generateComputedValuesForMap(values *map[string]data.Value, attribute *schema.Attribute) error { 121 | for key, value := range *values { 122 | // Then we're going to go through each value and check if it has any 123 | // attributes that need to be computed. 124 | newValue, err := generateComputedValue(value, attribute) 125 | if err != nil { 126 | return err 127 | } 128 | (*values)[key] = newValue 129 | } 130 | return nil 131 | } 132 | 133 | func generateComputedValuesForObject(values *map[string]data.Value, attributes map[string]schema.Attribute) error { 134 | for key, attribute := range attributes { 135 | if value, ok := (*values)[key]; ok { 136 | // This means we already have a value for this attribute, so we're 137 | // not going to generate a new one completely. But we do need to 138 | // recurse down into any objects as they maybe have generated 139 | // attributes. 140 | var err error 141 | if (*values)[key], err = generateComputedValue(value, &attribute); err != nil { 142 | return err 143 | } 144 | continue 145 | } 146 | 147 | if attribute.Value != nil { 148 | if !attribute.Computed { 149 | // If we didn't check this, it would just cause another error 150 | // later but at least here we can return a nice error message. 151 | return fmt.Errorf("attribute %s has specified a value in the json schema without being marked as computed", key) 152 | } 153 | var err error 154 | if (*values)[key], err = generateComputedValue(*attribute.Value, &attribute); err != nil { 155 | return err 156 | } 157 | } 158 | } 159 | 160 | return nil 161 | } 162 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "tfcoremock Provider" 4 | description: |- 5 | The tfcoremock provider is intended to aid with testing the Terraform core libraries and the Terraform CLI. This provider should allow users to define all possible Terraform configurations and run them through the Terraform core platform. 6 | The provider supplies two static resources: 7 | tfcoremock_simple_resourcetfcoremock_complex_resource 8 | Users can then define additional dynamic resources by supplying a dynamic_resources.json file alongside their root Terraform configuration. These dynamic resources can be used to model any Terraform configuration not covered by the provided static resources. 9 | By default, all resources created by the provider are then converted into a human-readable JSON format and written out to the resource directory. This behaviour can be disabled by turning on the use_only_state flag in the provider schema (this is useful when running the provider in a Terraform Cloud environment). The resource directory defaults to terraform.resource. 10 | All resources supplied by the provider (including the simple and complex resource as well as any dynamic resources) are duplicated into data sources. The data sources should be supplied in the JSON format that resources are written into. The provider looks into the data directory, which defaults to terraform.data. 11 | All resources (and data sources) supplied by the provider have an id attribute that is generated if not set by the configuration. Dynamic resources cannot define an id attribute as the provider will create one for them. The id attribute is used as the name of the human-readable JSON files held in the resource and data directories. 12 | Additionally, all resources are available to be queried via list blocks. For now only the id attribute is supported as a field to retrieve a specific instance. It is optional, so all resources of the specified type will be returned if the field is left blank. 13 | The provider also supports actions (introduced in Terraform v1.14). All resources (both static and dynamic) are made available as action blocks, that can be plugged into any Terraform configuration. Unlike resources and data sources, actions have no id associated with them as they are not written to disk. 14 | --- 15 | 16 | # tfcoremock Provider 17 | 18 | The `tfcoremock` provider is intended to aid with testing the Terraform core libraries and the Terraform CLI. This provider should allow users to define all possible Terraform configurations and run them through the Terraform core platform. 19 | 20 | The provider supplies two static resources: 21 | 22 | - `tfcoremock_simple_resource` 23 | - `tfcoremock_complex_resource` 24 | 25 | Users can then define additional dynamic resources by supplying a `dynamic_resources.json` file alongside their root Terraform configuration. These dynamic resources can be used to model any Terraform configuration not covered by the provided static resources. 26 | 27 | By default, all resources created by the provider are then converted into a human-readable JSON format and written out to the resource directory. This behaviour can be disabled by turning on the `use_only_state` flag in the provider schema (this is useful when running the provider in a Terraform Cloud environment). The resource directory defaults to `terraform.resource`. 28 | 29 | All resources supplied by the provider (including the simple and complex resource as well as any dynamic resources) are duplicated into data sources. The data sources should be supplied in the JSON format that resources are written into. The provider looks into the data directory, which defaults to `terraform.data`. 30 | 31 | All resources (and data sources) supplied by the provider have an `id` attribute that is generated if not set by the configuration. Dynamic resources cannot define an `id` attribute as the provider will create one for them. The `id` attribute is used as the name of the human-readable JSON files held in the resource and data directories. 32 | 33 | Additionally, all resources are available to be queried via `list` blocks. For now only the `id` attribute is supported as a field to retrieve a specific instance. It is optional, so all resources of the specified type will be returned if the field is left blank. 34 | 35 | The provider also supports actions (introduced in Terraform v1.14). All resources (both static and dynamic) are made available as action blocks, that can be plugged into any Terraform configuration. Unlike resources and data sources, actions have no `id` associated with them as they are not written to disk. 36 | 37 | ## Example Usage 38 | 39 | ```terraform 40 | provider "tfcoremock" { 41 | resource_directory = "terraform.resource" 42 | data_directory = "terraform.data" 43 | use_only_state = false 44 | } 45 | ``` 46 | 47 | 48 | ## Schema 49 | 50 | ### Optional 51 | 52 | - `data_directory` (String) The directory that the provider should use to read the human-readable JSON files for each requested data source. Defaults to `data.resource`. 53 | - `defer_changes` (List of String) If set, any resources with an ID in this list will have any changes deferred during the plan phase. 54 | - `fail_on_create` (List of String) If set, any resources with an ID in this list will fail during the create phase. 55 | - `fail_on_delete` (List of String) If set, any resources with an ID in this list will fail during the delete phase. 56 | - `fail_on_read` (List of String) If set, any resources with an ID in this list will fail during the read phase. 57 | - `fail_on_update` (List of String) If set, any resources with an ID in this list will fail during the update phase. 58 | - `resource_directory` (String) The directory that the provider should use to write the human-readable JSON files for each managed resource. If `use_only_state` is set to `true` then this value does not matter. Defaults to `terraform.resource`. 59 | - `use_only_state` (Boolean) If set to true the provider will rely only on the Terraform state file to load managed resources and will not write anything to disk. Defaults to `false`. 60 | -------------------------------------------------------------------------------- /internal/client/local.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package client 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "os" 13 | "path/filepath" 14 | 15 | "github.com/hashicorp/terraform-plugin-log/tflog" 16 | 17 | "github.com/hashicorp/terraform-provider-tfcoremock/internal/data" 18 | ) 19 | 20 | var _ Client = Local{} 21 | 22 | type Local struct { 23 | ResourceDirectory string 24 | DataDirectory string 25 | } 26 | 27 | func (local Local) ReadResource(ctx context.Context, id string) (*data.Resource, error) { 28 | tflog.Trace(ctx, "Local.ReadResource") 29 | 30 | jsonPath := filepath.Join(local.ResourceDirectory, fmt.Sprintf("%s.json", id)) 31 | 32 | jsonData, err := os.ReadFile(jsonPath) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | var value data.Resource 38 | if err := json.Unmarshal(jsonData, &value); err != nil { 39 | return nil, err 40 | } 41 | 42 | return &value, nil 43 | } 44 | 45 | func (local Local) WriteResource(ctx context.Context, value *data.Resource) error { 46 | tflog.Trace(ctx, "Local.WriteResource") 47 | 48 | jsonData, err := json.MarshalIndent(value, "", " ") 49 | if err != nil { 50 | return err 51 | } 52 | 53 | if err := os.MkdirAll(local.ResourceDirectory, 0700); err != nil { 54 | return err 55 | } 56 | 57 | jsonPath := filepath.Join(local.ResourceDirectory, fmt.Sprintf("%s.json", value.GetId())) 58 | 59 | // Let's just do a quick sanity check here. We are expecting the stat to 60 | // return an os.IsNotExist error, we want to make sure we aren't trying to 61 | // create a resource that already exists. If we don't get an error then that 62 | // means we are trying to overwrite a resource when we shouldn't, and if we 63 | // get anything other than an os.IsNotExist error then something even 64 | // weirder is happening. 65 | if _, err := os.Stat(jsonPath); err == nil { 66 | return errors.New("resource with the specified id likely already exists") 67 | } else if err != nil && !os.IsNotExist(err) { 68 | return err 69 | } 70 | 71 | if err := os.WriteFile(jsonPath, jsonData, 0644); err != nil { 72 | return err 73 | } 74 | 75 | return nil 76 | } 77 | 78 | func (local Local) UpdateResource(ctx context.Context, value *data.Resource) error { 79 | jsonData, err := json.MarshalIndent(value, "", " ") 80 | if err != nil { 81 | return err 82 | } 83 | 84 | jsonPath := filepath.Join(local.ResourceDirectory, fmt.Sprintf("%s.json", value.GetId())) 85 | if _, err := os.Stat(jsonPath); err != nil { 86 | return err 87 | } 88 | 89 | if err := os.WriteFile(jsonPath, jsonData, 0644); err != nil { 90 | return err 91 | } 92 | 93 | return nil 94 | } 95 | 96 | func (local Local) DeleteResource(ctx context.Context, id string) error { 97 | jsonPath := filepath.Join(local.ResourceDirectory, fmt.Sprintf("%s.json", id)) 98 | if err := os.Remove(jsonPath); err != nil { 99 | return err 100 | } 101 | 102 | // If the directory is empty after we've deleted this resource, let's tidy 103 | // up and delete the directory as well. 104 | resources, err := os.Open(local.ResourceDirectory) 105 | if err != nil { 106 | // Something weird has happened, but we're not going to fail the whole 107 | // delete operation just cos we couldn't clean up the directory. 108 | tflog.Info(ctx, fmt.Sprintf("couldn't open resource directory at (%s) to tidy up: %v", local.ResourceDirectory, err)) 109 | return nil 110 | } 111 | 112 | files, err := resources.Readdirnames(1) 113 | if len(files) > 0 { 114 | // Then we're not going to do anything, there are still other files or 115 | // resources within this directory. 116 | return nil 117 | } 118 | 119 | if err == io.EOF { 120 | // Then we returned an empty slice of files because the directory is 121 | // empty - let's delete the directory then. This is an acceptable 122 | // outcome, so we're not going to log anything. 123 | _ = os.Remove(local.ResourceDirectory) 124 | return nil 125 | } 126 | 127 | // Then something else caused us to return an empty slice. We'll be cautious 128 | // and log the error but not delete the directory. 129 | tflog.Info(ctx, fmt.Sprintf("failed to query if the resource directory at (%s) was empty: %v", local.ResourceDirectory, err)) 130 | return nil 131 | } 132 | 133 | func (local Local) ReadDataSource(ctx context.Context, id string) (*data.Resource, error) { 134 | jsonPath := filepath.Join(local.DataDirectory, fmt.Sprintf("%s.json", id)) 135 | 136 | jsonData, err := os.ReadFile(jsonPath) 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | var value data.Resource 142 | if err := json.Unmarshal(jsonData, &value); err != nil { 143 | return nil, err 144 | } 145 | 146 | return &value, nil 147 | } 148 | 149 | func (local Local) ListResources(ctx context.Context, typeName *string, id *string, yield func(resource *data.Resource, err error), limit int64) error { 150 | if id != nil { 151 | yield(local.ReadResource(ctx, *id)) 152 | return nil 153 | } 154 | 155 | entries, err := os.ReadDir(local.ResourceDirectory) 156 | if err != nil { 157 | return err 158 | } 159 | 160 | var count int64 161 | for _, entry := range entries { 162 | if count == limit { 163 | break // only yield the exact number of responses 164 | } 165 | 166 | if entry.IsDir() { 167 | continue // no nested directories 168 | } 169 | 170 | ext := filepath.Ext(entry.Name()) 171 | if ext != ".json" { 172 | continue // only read the json files 173 | } 174 | 175 | jsonData, err := os.ReadFile(filepath.Join(local.ResourceDirectory, entry.Name())) 176 | if err != nil { 177 | count++ 178 | yield(nil, fmt.Errorf("failed to read %s: %w", entry.Name(), err)) 179 | continue 180 | } 181 | 182 | var value data.Resource 183 | if err := json.Unmarshal(jsonData, &value); err != nil { 184 | count++ 185 | yield(nil, fmt.Errorf("failed to unmarshal %s: %w", entry.Name(), err)) 186 | continue 187 | } 188 | 189 | if typeName != nil && value.ResourceType != *typeName { 190 | continue // wrong type 191 | } 192 | 193 | count++ 194 | yield(&value, nil) 195 | } 196 | 197 | return nil 198 | } 199 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform `tfcoremock` Provider 2 | 3 | The `tfcoremock` provider is intended to aid with testing the Terraform core libraries 4 | and the Terraform CLI. This provider should allow users to define all possible 5 | Terraform configurations and run them through the Terraform core platform. 6 | 7 | The provider supplies two static resources: 8 | 9 | - `tfcoremock_simple_resource` 10 | - `tfcoremock_complex_resource` 11 | 12 | Users can then define additional dynamic resources by supplying a 13 | `dynamic_resources.json` file alongside their root Terraform configuration. 14 | These dynamic resources can be used to model any Terraform configuration not 15 | covered by the provided static resources. 16 | 17 | Use the `TFCOREMOCK_DYNAMIC_RESOURCES_FILE` environment variable to customise 18 | the location of the `dynamic_resources.json` file. By default, the provider 19 | looks for the `dynamic_resources.json` file in the same directory as the 20 | Terraform config files, but using this environment variable allows the dynamic 21 | resources to be defined by any file on the system. For example: 22 | `TFCOREMOCK_DYNAMIC_RESOURCES_FILE=/path/to/resources.json terraform plan` 23 | 24 | By default, all resources created by the provider are then converted into a 25 | human-readable JSON format and written out to the resource directory. This 26 | behaviour can be disabled by turning on the `use_only_state` flag in the 27 | provider schema (this is useful when running the provider in a Terraform Cloud 28 | environment). The resource directory defaults to `terraform.resource`. 29 | 30 | All resources supplied by the provider (including the simple and 31 | complex resource as well as any dynamic resources) are duplicated into data 32 | sources. The data sources should be supplied in the JSON format that resources 33 | are written into. The provider looks into the data directory, which defaults to 34 | `terraform.data`. 35 | 36 | All resources (and data sources) supplied by the provider have an `id` 37 | attribute that is generated if not set by the configuration. Dynamic resources 38 | cannot define an `id` attribute as the provider will create one for them. The 39 | `id` attribute is used as name of the human-readable JSON files held in the 40 | resource and data directories. 41 | 42 | Additionally, all resources are available to be queried via `list` blocks. For 43 | now only the `id` attribute is supported as a field to retrieve a specific 44 | instance. It is optional, so all resources of the specified type will be 45 | returned if the field is left blank. 46 | 47 | The provider also supports actions (introduced in Terraform v1.14). All 48 | resources (both static and dynamic) are made available as action blocks, that 49 | can be plugged into any Terraform configuration. Unlike resources and data 50 | sources, actions have no `id` associated with them as they are not written to 51 | disk. 52 | 53 | ## Requirements 54 | 55 | - [Terraform](https://www.terraform.io/downloads.html) >= 1.0 56 | - [Go](https://golang.org/doc/install) >= 1.21 57 | 58 | ## Using the provider 59 | 60 | We provide a simple example here. View the [examples](./examples) and 61 | [docs](./docs) subdirectories for more examples. 62 | 63 | In this example, we have a `tfcoremock_simple_resource` defined as a data source with 64 | an `id` of `my-simple-resource`. This means we create a file 65 | `terraform.data/my-simple-resource.json` which defines a simple resource with 66 | a single integer set. We then define a dynamic resource called 67 | `tfcoremock_dynamic_resource`. The dynamic resource holds a single integer, and is 68 | defined in the `dynamic_resources.json` file. 69 | 70 | Note, that we do not define an `id` field for this resource when we provide the 71 | definition. Despite this, we can still provide a value for the `id` in the 72 | configuration because the provider ensures that all resources have this attribute. 73 | In this example, we do provide a value for the `id` field. If we didn't, the provider 74 | would generate one for us. 75 | 76 | The following subsections show the Terraform configuration pre-apply and then 77 | show the extra files created post-apply. 78 | 79 | ### Pre-apply 80 | 81 | #### **./main.tf** 82 | ```hcl 83 | terraform { 84 | required_providers { 85 | tfcoremock = { 86 | source = "hashicorp/tfcoremock" 87 | version = "0.1.2" 88 | } 89 | } 90 | } 91 | 92 | data "tfcoremock_simple_resource" "my_simple_resource" { 93 | id = "my-simple-resource" 94 | } 95 | 96 | resource "tfcoremock_dynamic_resource" "my_dynamic_resource" { 97 | id = "my-dynamic-resource" 98 | my_value = data.tfcoremock_simple_resource.my_simple_resource.integer + 1 99 | } 100 | ``` 101 | 102 | #### **./terraform.data/my-simple-resource.json** 103 | ```json 104 | { 105 | "values": { 106 | "integer": { 107 | "number": "0" 108 | }, 109 | "id": { 110 | "string": "my-simple-resource" 111 | } 112 | } 113 | } 114 | ``` 115 | 116 | #### **./dynamic_resources.json** 117 | ```json 118 | { 119 | "tfcoremock_dynamic_resource": { 120 | "attributes": { 121 | "my_value": { 122 | "type": "integer", 123 | "required": true 124 | } 125 | } 126 | } 127 | } 128 | ``` 129 | 130 | ### Post apply 131 | 132 | In addition to the normal Terraform state and lock files, you will see the new 133 | resource we created has been written into the resource directory. 134 | 135 | #### **./terraform.resource/my-dynamic-resource.json** 136 | ```json 137 | { 138 | "values": { 139 | "id": { 140 | "string": "my-dynamic-resource" 141 | }, 142 | "my_value": { 143 | "number": "1" 144 | } 145 | } 146 | } 147 | ``` 148 | 149 | 150 | ## Developing the Provider 151 | 152 | If you wish to work on the provider, you'll first need 153 | [Go](http://www.golang.org) installed on your machine 154 | (see [Requirements](#requirements) above). 155 | 156 | To compile the provider, run `go install`. This will build the provider and put 157 | the provider binary in the `$GOPATH/bin` directory. 158 | 159 | To generate or update documentation, run `go generate`. 160 | 161 | In order to run the full suite of Acceptance tests, run `make testacc`. 162 | -------------------------------------------------------------------------------- /internal/schema/attribute.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package schema 5 | 6 | import ( 7 | "fmt" 8 | 9 | action_schema "github.com/hashicorp/terraform-plugin-framework/action/schema" 10 | datasource_schema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" 11 | resource_schema "github.com/hashicorp/terraform-plugin-framework/resource/schema" 12 | "github.com/pkg/errors" 13 | 14 | "github.com/hashicorp/terraform-provider-tfcoremock/internal/data" 15 | ) 16 | 17 | // Attribute defines an internal representation of a Terraform attribute in a 18 | // schema. 19 | // 20 | // It is designed to be read dynamically from a JSON object, allowing schemas, 21 | // blocks and attributes to be defined dynamically by the user of the provider. 22 | // 23 | // For data sources, the values provided in the optional, required and computed 24 | // field are ignored. All data source attributes are computed, with optional and 25 | // required being set to false regardless of the actual values for those fields. 26 | // The generated ID attribute is unique and marked as required and not computed 27 | // as it is used to identify the data source and retrieve it during the 28 | // Terraform operations. 29 | type Attribute struct { 30 | Description string `json:"-"` // Dynamic resources don't need descriptions so hide them from the exposed JSON schema. 31 | MarkdownDescription string `json:"-"` // Dynamic resources don't need descriptions so hide them from the exposed JSON schema. 32 | 33 | Type Type `json:"type"` 34 | Optional bool `json:"optional"` 35 | Required bool `json:"required"` 36 | Computed bool `json:"computed"` 37 | 38 | Value *data.Value `json:"value,omitempty"` 39 | 40 | List *Attribute `json:"list,omitempty"` 41 | Map *Attribute `json:"map,omitempty"` 42 | Object map[string]Attribute `json:"object,omitempty"` 43 | Set *Attribute `json:"set,omitempty"` 44 | 45 | Sensitive bool `json:"sensitive"` // True if values for this attribute should be hidden in the plan. 46 | Replace bool `json:"replace"` // True if the resource should be replaced when this attribute changes. 47 | 48 | // SkipNestedMetadata instructs the dynamic resource to not use the nested 49 | // attribute field when building element and attribute types of complex 50 | // attributes (list, map, object, and set). 51 | SkipNestedMetadata bool `json:"skip_nested_metadata"` 52 | } 53 | 54 | // AttributeTypes contains functions that map provider attributes into Terraform 55 | // resource or datasource attributes. 56 | type AttributeTypes[A any] struct { 57 | asBoolean func(attribute Attribute) (*A, error) 58 | asFloat func(attribute Attribute) (*A, error) 59 | asInteger func(attribute Attribute) (*A, error) 60 | asNumber func(attribute Attribute) (*A, error) 61 | asString func(attribute Attribute) (*A, error) 62 | 63 | asList func(attribute Attribute) (*A, error) 64 | asNestedList func(attribute Attribute) (*A, error) 65 | 66 | asMap func(attribute Attribute) (*A, error) 67 | asNestedMap func(attribute Attribute) (*A, error) 68 | 69 | asSet func(attribute Attribute) (*A, error) 70 | asNestedSet func(attribute Attribute) (*A, error) 71 | 72 | asObject func(attribute Attribute) (*A, error) 73 | asNestedObject func(attribute Attribute) (*A, error) 74 | } 75 | 76 | // ToTerraformAttribute converts our representation of an Attribute into a 77 | // Terraform SDK attribute so it can be passed back to Terraform Core in a 78 | // resource or data source schema. 79 | func ToTerraformAttribute[A any](a Attribute, types *AttributeTypes[A]) (*A, error) { 80 | switch a.Type { 81 | case Boolean: 82 | return types.asBoolean(a) 83 | case Float: 84 | return types.asFloat(a) 85 | case Integer: 86 | return types.asInteger(a) 87 | case Number: 88 | return types.asNumber(a) 89 | case String: 90 | return types.asString(a) 91 | case List: 92 | if !a.SkipNestedMetadata && a.List.Type == Object { 93 | return types.asNestedList(a) 94 | } 95 | return types.asList(a) 96 | case Map: 97 | if !a.SkipNestedMetadata && a.Map.Type == Object { 98 | return types.asNestedMap(a) 99 | } 100 | return types.asMap(a) 101 | case Set: 102 | if !a.SkipNestedMetadata && a.Set.Type == Object { 103 | return types.asNestedSet(a) 104 | } 105 | return types.asSet(a) 106 | case Object: 107 | if a.SkipNestedMetadata { 108 | return types.asObject(a) 109 | } 110 | 111 | return types.asNestedObject(a) 112 | case "": 113 | return nil, fmt.Errorf("missing attribute type") 114 | default: 115 | return nil, fmt.Errorf("unrecognized attribute type '%s'", a.Type) 116 | } 117 | } 118 | 119 | func attributesToTerraformResourceAttributes(attributes map[string]Attribute) (map[string]resource_schema.Attribute, error) { 120 | tfAttributes := make(map[string]resource_schema.Attribute) 121 | for name, attribute := range attributes { 122 | attribute, err := ToTerraformAttribute(attribute, resources) 123 | if err != nil { 124 | return nil, errors.Wrapf(err, "failed to create attribute '%s'", name) 125 | } 126 | tfAttributes[name] = *attribute 127 | } 128 | return tfAttributes, nil 129 | } 130 | 131 | func attributesToTerraformDataSourceAttributes(attributes map[string]Attribute) (map[string]datasource_schema.Attribute, error) { 132 | tfAttributes := make(map[string]datasource_schema.Attribute) 133 | for name, attribute := range attributes { 134 | attribute, err := ToTerraformAttribute(attribute, datasources) 135 | if err != nil { 136 | return nil, errors.Wrapf(err, "failed to create attribute '%s'", name) 137 | } 138 | tfAttributes[name] = *attribute 139 | } 140 | return tfAttributes, nil 141 | } 142 | 143 | func attributesToTerraformActionAttributes(attributes map[string]Attribute) (map[string]action_schema.Attribute, error) { 144 | tfAttributes := make(map[string]action_schema.Attribute) 145 | for name, attribute := range attributes { 146 | attribute, err := ToTerraformAttribute(attribute, actions) 147 | if err != nil { 148 | return nil, errors.Wrapf(err, "failed to create attribute '%s'", name) 149 | } 150 | tfAttributes[name] = *attribute 151 | } 152 | return tfAttributes, nil 153 | } 154 | -------------------------------------------------------------------------------- /internal/schema/block.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package schema 5 | 6 | import ( 7 | "fmt" 8 | 9 | action_schema "github.com/hashicorp/terraform-plugin-framework/action/schema" 10 | datasource_schema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" 11 | resource_schema "github.com/hashicorp/terraform-plugin-framework/resource/schema" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | const ( 16 | NestingModeList = "list" 17 | NestingModeSet = "set" 18 | ) 19 | 20 | // Block defines an internal representation of a Terraform block in a schema. 21 | // 22 | // It is designed to be read dynamically from a JSON object, allowing schemas, 23 | // blocks and attributes to be defined dynamically by the user of the provider. 24 | type Block struct { 25 | Description string `json:"-"` // Dynamic resources don't need descriptions so hide them from the exposed JSON schema. 26 | MarkdownDescription string `json:"-"` // Dynamic resources don't need descriptions so hide them from the exposed JSON schema. 27 | 28 | Attributes map[string]Attribute `json:"attributes"` 29 | Blocks map[string]Block `json:"blocks"` 30 | Mode string `json:"mode"` 31 | } 32 | 33 | type ToListBlock[B any, A any] func(block Block, blocks map[string]B, attributes map[string]A) *B 34 | type ToSetBlock[B any, A any] func(block Block, blocks map[string]B, attributes map[string]A) *B 35 | 36 | // ToTerraformBlock converts our representation of a Block into a Terraform SDK 37 | // block so it can be passed back to Terraform Core in a resource or data source 38 | // schema. 39 | func ToTerraformBlock[B, A any](b Block, toListBlock ToListBlock[B, A], toSetBlock ToSetBlock[B, A], attributeTypes *AttributeTypes[A]) (*B, error) { 40 | tfAttributes := make(map[string]A) 41 | tfBlocks := make(map[string]B) 42 | 43 | for name, attribute := range b.Attributes { 44 | attribute, err := ToTerraformAttribute(attribute, attributeTypes) 45 | if err != nil { 46 | return nil, errors.Wrapf(err, "failed to create attribute '%s'", name) 47 | } 48 | tfAttributes[name] = *attribute 49 | } 50 | 51 | for name, block := range b.Blocks { 52 | block, err := ToTerraformBlock(block, toListBlock, toSetBlock, attributeTypes) 53 | if err != nil { 54 | return nil, errors.Wrapf(err, "failed to create block '%s'", name) 55 | } 56 | tfBlocks[name] = *block 57 | } 58 | 59 | switch b.Mode { 60 | case "", NestingModeList: 61 | return toListBlock(b, tfBlocks, tfAttributes), nil 62 | case NestingModeSet: 63 | return toSetBlock(b, tfBlocks, tfAttributes), nil 64 | default: 65 | return nil, fmt.Errorf("invalid nesting mode '%s'", b.Mode) 66 | } 67 | } 68 | 69 | func blocksToTerraformResourceBlocks(blocks map[string]Block) (map[string]resource_schema.Block, error) { 70 | toListBlock := func(block Block, blocks map[string]resource_schema.Block, attributes map[string]resource_schema.Attribute) *resource_schema.Block { 71 | var tfBlock resource_schema.Block 72 | tfBlock = resource_schema.ListNestedBlock{ 73 | Description: block.Description, 74 | MarkdownDescription: block.MarkdownDescription, 75 | NestedObject: resource_schema.NestedBlockObject{ 76 | Attributes: attributes, 77 | Blocks: blocks, 78 | }, 79 | } 80 | return &tfBlock 81 | } 82 | 83 | toSetBlock := func(block Block, blocks map[string]resource_schema.Block, attributes map[string]resource_schema.Attribute) *resource_schema.Block { 84 | var tfBlock resource_schema.Block 85 | tfBlock = resource_schema.SetNestedBlock{ 86 | Description: block.Description, 87 | MarkdownDescription: block.MarkdownDescription, 88 | NestedObject: resource_schema.NestedBlockObject{ 89 | Attributes: attributes, 90 | Blocks: blocks, 91 | }, 92 | } 93 | return &tfBlock 94 | } 95 | 96 | tfBlocks := make(map[string]resource_schema.Block) 97 | for name, block := range blocks { 98 | block, err := ToTerraformBlock(block, toListBlock, toSetBlock, resources) 99 | if err != nil { 100 | return nil, errors.Wrapf(err, "failed to create block '%s'", name) 101 | } 102 | tfBlocks[name] = *block 103 | } 104 | return tfBlocks, nil 105 | } 106 | 107 | func blocksToTerraformDataSourceBlocks(blocks map[string]Block) (map[string]datasource_schema.Block, error) { 108 | toListBlock := func(block Block, blocks map[string]datasource_schema.Block, attributes map[string]datasource_schema.Attribute) *datasource_schema.Block { 109 | var tfBlock datasource_schema.Block 110 | tfBlock = datasource_schema.ListNestedBlock{ 111 | Description: block.Description, 112 | MarkdownDescription: block.MarkdownDescription, 113 | NestedObject: datasource_schema.NestedBlockObject{ 114 | Attributes: attributes, 115 | Blocks: blocks, 116 | }, 117 | } 118 | return &tfBlock 119 | } 120 | 121 | toSetBlock := func(block Block, blocks map[string]datasource_schema.Block, attributes map[string]datasource_schema.Attribute) *datasource_schema.Block { 122 | var tfBlock datasource_schema.Block 123 | tfBlock = datasource_schema.SetNestedBlock{ 124 | Description: block.Description, 125 | MarkdownDescription: block.MarkdownDescription, 126 | NestedObject: datasource_schema.NestedBlockObject{ 127 | Attributes: attributes, 128 | Blocks: blocks, 129 | }, 130 | } 131 | return &tfBlock 132 | } 133 | 134 | tfBlocks := make(map[string]datasource_schema.Block) 135 | for name, block := range blocks { 136 | block, err := ToTerraformBlock(block, toListBlock, toSetBlock, datasources) 137 | if err != nil { 138 | return nil, errors.Wrapf(err, "failed to create block '%s'", name) 139 | } 140 | tfBlocks[name] = *block 141 | } 142 | return tfBlocks, nil 143 | } 144 | 145 | func blocksToTerraformActionBlocks(blocks map[string]Block) (map[string]action_schema.Block, error) { 146 | toListBlock := func(block Block, blocks map[string]action_schema.Block, attributes map[string]action_schema.Attribute) *action_schema.Block { 147 | var tfBlock action_schema.Block 148 | tfBlock = action_schema.ListNestedBlock{ 149 | Description: block.Description, 150 | MarkdownDescription: block.MarkdownDescription, 151 | NestedObject: action_schema.NestedBlockObject{ 152 | Attributes: attributes, 153 | Blocks: blocks, 154 | }, 155 | } 156 | return &tfBlock 157 | } 158 | 159 | toSetBlock := func(block Block, blocks map[string]action_schema.Block, attributes map[string]action_schema.Attribute) *action_schema.Block { 160 | var tfBlock action_schema.Block 161 | tfBlock = action_schema.SetNestedBlock{ 162 | Description: block.Description, 163 | MarkdownDescription: block.MarkdownDescription, 164 | NestedObject: action_schema.NestedBlockObject{ 165 | Attributes: attributes, 166 | Blocks: blocks, 167 | }, 168 | } 169 | return &tfBlock 170 | } 171 | 172 | tfBlocks := make(map[string]action_schema.Block) 173 | for name, block := range blocks { 174 | block, err := ToTerraformBlock(block, toListBlock, toSetBlock, actions) 175 | if err != nil { 176 | return nil, errors.Wrapf(err, "failed to create block '%s'", name) 177 | } 178 | tfBlocks[name] = *block 179 | } 180 | return tfBlocks, nil 181 | } 182 | -------------------------------------------------------------------------------- /internal/schema/complex/complex.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package complex 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/hashicorp/terraform-provider-tfcoremock/internal/schema" 11 | ) 12 | 13 | var ( 14 | description = `A complex resource that contains five basic attributes, four complex attributes, and two nested blocks. 15 | 16 | The five basic attributes are boolean, number, string, float, and integer (as with the tfcoremock_simple_resource). 17 | 18 | The complex attributes are a map, a list, a set, and an object. The object type contains the same set of attributes as the schema itself, making a recursive structure. The list, set and map all contain objects which are also recursive. Blocks cannot go into attributes, so the complex attributes do not recurse on the block types. 19 | 20 | The blocks are a nested list and a nested set. The blocks contain the same set of attributes and blocks as the schema itself, also making a recursive structure. Note, blocks contain both attributes and more blocks so the block types are fully recursive. 21 | 22 | The complex and block types are nested %d times, at the leaf level of recursion the complex attributes and blocks only contain the simple (ie. non-recursive) attributes. This prevents a potentially infinite level of recursion.` 23 | markdownDescription = `A complex resource that contains five basic attributes, four complex attributes, and two nested blocks. 24 | 25 | The five basic attributes are ''boolean'', ''number'', ''string'', ''float'', and ''integer'' (as with the ''tfcoremock_simple_resource''). 26 | 27 | The complex attributes are a ''map'', a ''list'', a ''set'', and an ''object''. The ''object'' type contains the same set of attributes as the schema itself, making a recursive structure. The ''list'', ''set'' and ''map'' all contain objects which are also recursive. Blocks cannot go into attributes, so the complex attributes do not recurse on the block types. 28 | 29 | The blocks are a nested ''list_block'' and a nested ''set_block''. The blocks contain the same set of attributes and blocks as the schema itself, also making a recursive structure. Note, blocks contain both attributes and more blocks so the block types are fully recursive. 30 | 31 | The complex and block types are nested %d times, at the leaf level of recursion the complex attributes and blocks only contain the simple (ie. non-recursive) attributes. This prevents a potentially infinite level of recursion.` 32 | ) 33 | 34 | func Schema(maxDepth int) schema.Schema { 35 | return schema.Schema{ 36 | Description: fmt.Sprintf(description, maxDepth), 37 | MarkdownDescription: strings.ReplaceAll(fmt.Sprintf(markdownDescription, maxDepth), "''", "`"), 38 | Attributes: attributes(0, maxDepth), 39 | Blocks: blocks(0, maxDepth), 40 | } 41 | } 42 | 43 | func blocks(depth, maxDepth int) map[string]schema.Block { 44 | if depth == maxDepth { 45 | return nil 46 | } 47 | 48 | blks := make(map[string]schema.Block) 49 | 50 | blks["list_block"] = schema.Block{ 51 | Description: "A list block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.", 52 | MarkdownDescription: "A list block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.", 53 | Attributes: attributes(depth+1, maxDepth), 54 | Blocks: blocks(depth+1, maxDepth), 55 | Mode: schema.NestingModeList, 56 | } 57 | 58 | blks["set_block"] = schema.Block{ 59 | Description: "A set block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.", 60 | MarkdownDescription: "A set block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.", 61 | Attributes: attributes(depth+1, maxDepth), 62 | Blocks: blocks(depth+1, maxDepth), 63 | Mode: schema.NestingModeSet, 64 | } 65 | 66 | return blks 67 | } 68 | 69 | func attributes(depth, maxDepth int) map[string]schema.Attribute { 70 | attrs := map[string]schema.Attribute{ 71 | "bool": { 72 | Description: "An optional boolean attribute, can be true or false.", 73 | MarkdownDescription: "An optional boolean attribute, can be true or false.", 74 | Optional: true, 75 | Type: schema.Boolean, 76 | }, 77 | "number": { 78 | Description: "An optional number attribute, can be an integer or a float.", 79 | MarkdownDescription: "An optional number attribute, can be an integer or a float.", 80 | Optional: true, 81 | Type: schema.Number, 82 | }, 83 | "string": { 84 | Description: "An optional string attribute.", 85 | MarkdownDescription: "An optional string attribute.", 86 | Optional: true, 87 | Type: schema.String, 88 | }, 89 | "float": { 90 | Description: "An optional float attribute.", 91 | MarkdownDescription: "An optional float attribute.", 92 | Optional: true, 93 | Type: schema.Float, 94 | }, 95 | "integer": { 96 | Description: "An optional integer attribute.", 97 | MarkdownDescription: "An optional integer attribute.", 98 | Optional: true, 99 | Type: schema.Integer, 100 | }, 101 | } 102 | 103 | if depth < maxDepth { 104 | attrs["list"] = schema.Attribute{ 105 | Description: "A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.", 106 | MarkdownDescription: "A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.", 107 | Optional: true, 108 | Type: schema.List, 109 | List: &schema.Attribute{ 110 | Type: schema.Object, 111 | Object: attributes(depth+1, maxDepth), 112 | }, 113 | } 114 | attrs["map"] = schema.Attribute{ 115 | Description: "A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.", 116 | MarkdownDescription: "A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.", 117 | Optional: true, 118 | Type: schema.Map, 119 | Map: &schema.Attribute{ 120 | Type: schema.Object, 121 | Object: attributes(depth+1, maxDepth), 122 | }, 123 | } 124 | attrs["object"] = schema.Attribute{ 125 | Description: "An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.", 126 | MarkdownDescription: "An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.", 127 | Optional: true, 128 | Type: schema.Object, 129 | Object: attributes(depth+1, maxDepth), 130 | } 131 | attrs["set"] = schema.Attribute{ 132 | Description: "A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.", 133 | MarkdownDescription: "A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.", 134 | Optional: true, 135 | Type: schema.Set, 136 | Set: &schema.Attribute{ 137 | Type: schema.Object, 138 | Object: attributes(depth+1, maxDepth), 139 | }, 140 | } 141 | } 142 | 143 | return attrs 144 | } 145 | -------------------------------------------------------------------------------- /internal/resource/resource.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 | "os" 10 | "slices" 11 | 12 | "github.com/hashicorp/go-uuid" 13 | "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" 14 | 15 | "github.com/hashicorp/terraform-plugin-framework/diag" 16 | "github.com/hashicorp/terraform-plugin-framework/path" 17 | "github.com/hashicorp/terraform-plugin-framework/resource" 18 | "github.com/hashicorp/terraform-plugin-go/tftypes" 19 | 20 | "github.com/hashicorp/terraform-provider-tfcoremock/internal/computed" 21 | 22 | "github.com/hashicorp/terraform-provider-tfcoremock/internal/client" 23 | "github.com/hashicorp/terraform-provider-tfcoremock/internal/data" 24 | "github.com/hashicorp/terraform-provider-tfcoremock/internal/schema" 25 | ) 26 | 27 | var _ resource.Resource = Resource{} 28 | var _ resource.ResourceWithIdentity = Resource{} 29 | var _ resource.ResourceWithImportState = Resource{} 30 | var _ resource.ResourceWithModifyPlan = Resource{} 31 | 32 | type Resource struct { 33 | Name string 34 | InternalSchema schema.Schema 35 | Client client.Client 36 | 37 | FailOnDelete []string 38 | FailOnCreate []string 39 | FailOnRead []string 40 | FailOnUpdate []string 41 | DeferChanges []string 42 | } 43 | 44 | func (r Resource) Metadata(ctx context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { 45 | response.TypeName = r.Name 46 | } 47 | 48 | func (r Resource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { 49 | var err error 50 | if response.Schema, err = r.InternalSchema.ToTerraformResourceSchema(); err != nil { 51 | response.Diagnostics.Append(diag.NewErrorDiagnostic(fmt.Sprintf("failed to build resource schema for '%s'", r.Name), err.Error())) 52 | } 53 | } 54 | 55 | func (r Resource) IdentitySchema(ctx context.Context, request resource.IdentitySchemaRequest, response *resource.IdentitySchemaResponse) { 56 | response.IdentitySchema = identityschema.Schema{ 57 | Attributes: map[string]identityschema.Attribute{ 58 | "id": identityschema.StringAttribute{ 59 | RequiredForImport: true, 60 | Description: "The ID of the resource.", 61 | }, 62 | }, 63 | } 64 | } 65 | 66 | func (r Resource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { 67 | resource := &data.Resource{} 68 | response.Diagnostics.Append(request.Plan.Get(ctx, &resource)...) 69 | if response.Diagnostics.HasError() { 70 | return 71 | } 72 | resource.ResourceType = r.Name 73 | 74 | // The root ID is a special computed value. 75 | if _, ok := resource.Values["id"]; !ok { 76 | id, err := uuid.GenerateUUID() 77 | if err != nil { 78 | response.Diagnostics.Append(diag.NewErrorDiagnostic("failed to generate id", err.Error())) 79 | return 80 | } 81 | resource.Values["id"] = data.Value{ 82 | String: &id, 83 | } 84 | } 85 | 86 | // Now go and do the rest of the computed values. 87 | if err := computed.GenerateComputedValues(resource, r.InternalSchema); err != nil { 88 | response.Diagnostics.Append(diag.NewErrorDiagnostic("failed to generate computed values", err.Error())) 89 | return 90 | } 91 | 92 | if slices.Contains(r.FailOnCreate, resource.GetId()) { 93 | response.Diagnostics.Append(diag.NewErrorDiagnostic("failed to create resource", "forced failure")) 94 | return 95 | } 96 | 97 | if err := r.Client.WriteResource(ctx, resource); err != nil { 98 | response.Diagnostics.Append(diag.NewErrorDiagnostic("failed to write resource", err.Error())) 99 | return 100 | } 101 | 102 | response.Diagnostics.Append(response.State.Set(ctx, resource)...) 103 | response.Diagnostics.Append(response.Identity.Set(ctx, resource.Identity())...) 104 | } 105 | 106 | func (r Resource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { 107 | resource := &data.Resource{} 108 | response.Diagnostics.Append(request.State.Get(ctx, &resource)...) 109 | if response.Diagnostics.HasError() { 110 | return 111 | } 112 | 113 | if slices.Contains(r.FailOnRead, resource.GetId()) { 114 | response.Diagnostics.Append(diag.NewErrorDiagnostic("failed to read resource", "forced failure")) 115 | return 116 | } 117 | 118 | data, err := r.Client.ReadResource(ctx, resource.GetId()) 119 | if err != nil { 120 | if os.IsNotExist(err) { 121 | // This is a bit of weird one as it means we tried to read a file 122 | // that doesn't exist but Terraform thinks it does. We treat this 123 | // as "drift" and let the Terraform framework handle it. 124 | response.State.RemoveResource(ctx) 125 | response.Diagnostics.Append(response.Identity.Set(ctx, resource.Identity())...) 126 | return 127 | } 128 | response.Diagnostics.AddError("failed to read resource", err.Error()) 129 | return 130 | } 131 | 132 | if data == nil { 133 | // The client returned a nil object with no error. This means it is 134 | // telling us to just rely on the state. 135 | data = resource 136 | } 137 | 138 | typ := request.State.Schema.Type().TerraformType(ctx) 139 | response.Diagnostics.Append(response.State.Set(ctx, data.WithType(typ.(tftypes.Object)))...) 140 | response.Diagnostics.Append(response.Identity.Set(ctx, data.Identity())...) 141 | } 142 | 143 | func (r Resource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { 144 | resource := &data.Resource{} 145 | response.Diagnostics.Append(request.Plan.Get(ctx, &resource)...) 146 | if response.Diagnostics.HasError() { 147 | return 148 | } 149 | resource.ResourceType = r.Name 150 | 151 | if err := computed.GenerateComputedValues(resource, r.InternalSchema); err != nil { 152 | response.Diagnostics.Append(diag.NewErrorDiagnostic("failed to generate computed values", err.Error())) 153 | return 154 | } 155 | 156 | if slices.Contains(r.FailOnUpdate, resource.GetId()) { 157 | response.Diagnostics.Append(diag.NewErrorDiagnostic("failed to update resource", "forced failure")) 158 | return 159 | } 160 | 161 | if err := r.Client.UpdateResource(ctx, resource); err != nil { 162 | response.Diagnostics.AddError("failed to update resource", err.Error()) 163 | return 164 | } 165 | 166 | response.Diagnostics.Append(response.State.Set(ctx, resource)...) 167 | response.Diagnostics.Append(response.Identity.Set(ctx, resource.Identity())...) 168 | } 169 | 170 | func (r Resource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { 171 | resource := &data.Resource{} 172 | response.Diagnostics.Append(request.State.Get(ctx, &resource)...) 173 | if response.Diagnostics.HasError() { 174 | return 175 | } 176 | 177 | if slices.Contains(r.FailOnDelete, resource.GetId()) { 178 | response.Diagnostics.Append(diag.NewErrorDiagnostic("failed to delete resource", "forced failure")) 179 | return 180 | } 181 | 182 | if err := r.Client.DeleteResource(ctx, resource.GetId()); err != nil { 183 | response.Diagnostics.AddError("failed to delete resource", err.Error()) 184 | return 185 | } 186 | 187 | response.State.RemoveResource(ctx) 188 | response.Diagnostics.Append(response.Identity.Set(ctx, resource.Identity())...) 189 | } 190 | 191 | func (r Resource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { 192 | resource.ImportStatePassthroughID(ctx, path.Root("id"), request, response) 193 | } 194 | 195 | func (r Resource) ModifyPlan(ctx context.Context, request resource.ModifyPlanRequest, response *resource.ModifyPlanResponse) { 196 | res := &data.Resource{} 197 | response.Diagnostics.Append(request.Plan.Get(ctx, &res)...) 198 | if response.Diagnostics.HasError() { 199 | return 200 | } 201 | 202 | if _, ok := res.Values["id"]; !ok { 203 | // then resource is unknown or something, which means we can't check 204 | // it 205 | return 206 | } 207 | 208 | id := res.GetId() 209 | if slices.Contains(r.DeferChanges, id) { 210 | // Then we want to defer this change! 211 | 212 | if !request.ClientCapabilities.DeferralAllowed { 213 | response.Diagnostics.AddAttributeError(path.Root("id"), "Invalid resource deferral", "This `id` was marked as \"should be deferred\", but the current version of Terraform does not support deferrals.") 214 | return 215 | } 216 | 217 | response.Deferred = &resource.Deferred{ 218 | // not technically true, but the closest we have 219 | Reason: resource.DeferredReasonResourceConfigUnknown, 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /internal/schema/datasource.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package schema 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-framework/attr" 8 | "github.com/hashicorp/terraform-plugin-framework/datasource/schema" 9 | ) 10 | 11 | var ( 12 | datasources = &AttributeTypes[schema.Attribute]{} 13 | ) 14 | 15 | func init() { 16 | datasources.asBoolean = asDataSourceBool 17 | datasources.asFloat = asDataSourceFloat 18 | datasources.asInteger = asDataSourceInteger 19 | datasources.asNumber = asDataSourceNumber 20 | datasources.asString = asDataSourceString 21 | datasources.asList = asDataSourceList 22 | datasources.asNestedList = asDataSourceNestedList 23 | datasources.asMap = asDataSourceMap 24 | datasources.asNestedMap = asDataSourceNestedMap 25 | datasources.asSet = asDataSourceSet 26 | datasources.asNestedSet = asDataSourceNestedSet 27 | datasources.asObject = asDataSourceObject 28 | datasources.asNestedObject = asDataSourceNestedObject 29 | } 30 | 31 | func asDataSourceBool(attribute Attribute) (*schema.Attribute, error) { 32 | tfAttribute := schema.BoolAttribute{ 33 | Description: attribute.Description, 34 | MarkdownDescription: attribute.MarkdownDescription, 35 | Optional: false, 36 | Required: false, 37 | Computed: true, 38 | Sensitive: attribute.Sensitive, 39 | } 40 | 41 | var out schema.Attribute 42 | out = tfAttribute 43 | return &out, nil 44 | } 45 | 46 | func asDataSourceFloat(attribute Attribute) (*schema.Attribute, error) { 47 | tfAttribute := schema.Float64Attribute{ 48 | Description: attribute.Description, 49 | MarkdownDescription: attribute.MarkdownDescription, 50 | Optional: false, 51 | Required: false, 52 | Computed: true, 53 | Sensitive: attribute.Sensitive, 54 | } 55 | 56 | var out schema.Attribute 57 | out = tfAttribute 58 | return &out, nil 59 | } 60 | 61 | func asDataSourceInteger(attribute Attribute) (*schema.Attribute, error) { 62 | tfAttribute := schema.Int64Attribute{ 63 | Description: attribute.Description, 64 | MarkdownDescription: attribute.MarkdownDescription, 65 | Optional: false, 66 | Required: false, 67 | Computed: true, 68 | Sensitive: attribute.Sensitive, 69 | } 70 | 71 | var out schema.Attribute 72 | out = tfAttribute 73 | return &out, nil 74 | } 75 | 76 | func asDataSourceNumber(attribute Attribute) (*schema.Attribute, error) { 77 | tfAttribute := schema.NumberAttribute{ 78 | Description: attribute.Description, 79 | MarkdownDescription: attribute.MarkdownDescription, 80 | Optional: false, 81 | Required: false, 82 | Computed: true, 83 | Sensitive: attribute.Sensitive, 84 | } 85 | 86 | var out schema.Attribute 87 | out = tfAttribute 88 | return &out, nil 89 | } 90 | 91 | func asDataSourceString(attribute Attribute) (*schema.Attribute, error) { 92 | tfAttribute := schema.StringAttribute{ 93 | Description: attribute.Description, 94 | MarkdownDescription: attribute.MarkdownDescription, 95 | Optional: false, 96 | Required: false, 97 | Computed: true, 98 | Sensitive: attribute.Sensitive, 99 | } 100 | 101 | var out schema.Attribute 102 | out = tfAttribute 103 | return &out, nil 104 | } 105 | 106 | func asDataSourceList(attribute Attribute) (*schema.Attribute, error) { 107 | tfAttribute := schema.ListAttribute{ 108 | Description: attribute.Description, 109 | MarkdownDescription: attribute.MarkdownDescription, 110 | Optional: false, 111 | Required: false, 112 | Computed: true, 113 | Sensitive: attribute.Sensitive, 114 | } 115 | 116 | elem, err := ToTerraformAttribute(*attribute.List, datasources) 117 | if err != nil { 118 | return nil, err 119 | } 120 | tfAttribute.ElementType = (*elem).GetType() 121 | 122 | var out schema.Attribute 123 | out = tfAttribute 124 | return &out, nil 125 | } 126 | 127 | func asDataSourceNestedList(attribute Attribute) (*schema.Attribute, error) { 128 | tfAttribute := schema.ListNestedAttribute{ 129 | Description: attribute.Description, 130 | MarkdownDescription: attribute.MarkdownDescription, 131 | Optional: false, 132 | Required: false, 133 | Computed: true, 134 | Sensitive: attribute.Sensitive, 135 | } 136 | 137 | var err error 138 | if tfAttribute.NestedObject.Attributes, err = attributesToTerraformDataSourceAttributes(attribute.List.Object); err != nil { 139 | return nil, err 140 | } 141 | 142 | var out schema.Attribute 143 | out = tfAttribute 144 | return &out, nil 145 | } 146 | 147 | func asDataSourceMap(attribute Attribute) (*schema.Attribute, error) { 148 | tfAttribute := schema.MapAttribute{ 149 | Description: attribute.Description, 150 | MarkdownDescription: attribute.MarkdownDescription, 151 | Optional: false, 152 | Required: false, 153 | Computed: true, 154 | Sensitive: attribute.Sensitive, 155 | } 156 | 157 | elem, err := ToTerraformAttribute(*attribute.Map, datasources) 158 | if err != nil { 159 | return nil, err 160 | } 161 | tfAttribute.ElementType = (*elem).GetType() 162 | 163 | var out schema.Attribute 164 | out = tfAttribute 165 | return &out, nil 166 | } 167 | 168 | func asDataSourceNestedMap(attribute Attribute) (*schema.Attribute, error) { 169 | tfAttribute := schema.MapNestedAttribute{ 170 | Description: attribute.Description, 171 | MarkdownDescription: attribute.MarkdownDescription, 172 | Optional: false, 173 | Required: false, 174 | Computed: true, 175 | Sensitive: attribute.Sensitive, 176 | } 177 | 178 | var err error 179 | if tfAttribute.NestedObject.Attributes, err = attributesToTerraformDataSourceAttributes(attribute.Map.Object); err != nil { 180 | return nil, err 181 | } 182 | 183 | var out schema.Attribute 184 | out = tfAttribute 185 | return &out, nil 186 | } 187 | 188 | func asDataSourceSet(attribute Attribute) (*schema.Attribute, error) { 189 | tfAttribute := schema.SetAttribute{ 190 | Description: attribute.Description, 191 | MarkdownDescription: attribute.MarkdownDescription, 192 | Optional: false, 193 | Required: false, 194 | Computed: true, 195 | Sensitive: attribute.Sensitive, 196 | } 197 | 198 | elem, err := ToTerraformAttribute(*attribute.Set, datasources) 199 | if err != nil { 200 | return nil, err 201 | } 202 | tfAttribute.ElementType = (*elem).GetType() 203 | 204 | var out schema.Attribute 205 | out = tfAttribute 206 | return &out, nil 207 | } 208 | 209 | func asDataSourceNestedSet(attribute Attribute) (*schema.Attribute, error) { 210 | tfAttribute := schema.SetNestedAttribute{ 211 | Description: attribute.Description, 212 | MarkdownDescription: attribute.MarkdownDescription, 213 | Optional: false, 214 | Required: false, 215 | Computed: true, 216 | Sensitive: attribute.Sensitive, 217 | } 218 | 219 | var err error 220 | if tfAttribute.NestedObject.Attributes, err = attributesToTerraformDataSourceAttributes(attribute.Set.Object); err != nil { 221 | return nil, err 222 | } 223 | 224 | var out schema.Attribute 225 | out = tfAttribute 226 | return &out, nil 227 | } 228 | 229 | func asDataSourceObject(attribute Attribute) (*schema.Attribute, error) { 230 | tfAttribute := schema.ObjectAttribute{ 231 | Description: attribute.Description, 232 | MarkdownDescription: attribute.MarkdownDescription, 233 | Optional: false, 234 | Required: false, 235 | Computed: true, 236 | Sensitive: attribute.Sensitive, 237 | } 238 | 239 | types := make(map[string]attr.Type) 240 | attributes, err := attributesToTerraformDataSourceAttributes(attribute.Object) 241 | if err != nil { 242 | return nil, err 243 | } 244 | for key, value := range attributes { 245 | types[key] = value.GetType() 246 | } 247 | tfAttribute.AttributeTypes = types 248 | 249 | var out schema.Attribute 250 | out = tfAttribute 251 | return &out, nil 252 | } 253 | 254 | func asDataSourceNestedObject(attribute Attribute) (*schema.Attribute, error) { 255 | tfAttribute := schema.SingleNestedAttribute{ 256 | Description: attribute.Description, 257 | MarkdownDescription: attribute.MarkdownDescription, 258 | Optional: false, 259 | Required: false, 260 | Computed: true, 261 | Sensitive: attribute.Sensitive, 262 | } 263 | 264 | var err error 265 | if tfAttribute.Attributes, err = attributesToTerraformDataSourceAttributes(attribute.Object); err != nil { 266 | return nil, err 267 | } 268 | 269 | var out schema.Attribute 270 | out = tfAttribute 271 | return &out, nil 272 | } 273 | -------------------------------------------------------------------------------- /internal/data/value.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package data 5 | 6 | import ( 7 | "errors" 8 | "math/big" 9 | 10 | "github.com/hashicorp/terraform-plugin-go/tftypes" 11 | ) 12 | 13 | // Value is the mock provider's representation of any generic Terraform Value. 14 | // 15 | // It can be converted from/to a tftypes.Value using the functions in this 16 | // package, and it can be marshalled to/from JSON using the Golang JSON package. 17 | // 18 | // Only a single field in the struct will be set at a given time. 19 | // 20 | // We use pointers where appropriate to make sure the omitempty metadata works 21 | // to keep the produced structs as small and relevant as possible as they are 22 | // intended to be consumed by humans. 23 | // 24 | // We introduce pointers to the complex objects because there is a difference 25 | // between unset (or nil) and an empty list and we want to record that 26 | // difference. 27 | type Value struct { 28 | Boolean *bool `json:"boolean,omitempty"` 29 | Number *big.Float `json:"number,omitempty"` 30 | String *string `json:"string,omitempty"` 31 | 32 | List *[]Value `json:"list,omitempty"` 33 | Map *map[string]Value `json:"map,omitempty"` 34 | Object *map[string]Value `json:"object,omitempty"` 35 | Set *[]Value `json:"set,omitempty"` 36 | } 37 | 38 | // ToTerraform5Value accepts our representation of a Value alongside the 39 | // tftypes.Type description, and returns a tftypes.Value object that can be 40 | // passed into the Terraform SDK. 41 | func ToTerraform5Value(v Value, t tftypes.Type) (tftypes.Value, error) { 42 | switch { 43 | case t.Is(tftypes.Bool): 44 | return tftypes.NewValue(tftypes.Bool, v.Boolean), nil 45 | case t.Is(tftypes.String): 46 | return tftypes.NewValue(tftypes.String, v.String), nil 47 | case t.Is(tftypes.Number): 48 | return tftypes.NewValue(tftypes.Number, v.Number), nil 49 | case t.Is(tftypes.List{}): 50 | return listToTerraform5Value(v.List, t.(tftypes.List)) 51 | case t.Is(tftypes.Map{}): 52 | return mapToTerraform5Value(v.Map, t.(tftypes.Map)) 53 | case t.Is(tftypes.Object{}): 54 | object, err := objectToTerraform5Value(v.Object, t.(tftypes.Object)) 55 | if err != nil { 56 | return tftypes.Value{}, err 57 | } 58 | return tftypes.NewValue(t, object), nil 59 | case t.Is(tftypes.Set{}): 60 | return setToTerraform5Value(v.Set, t.(tftypes.Set)) 61 | default: 62 | return tftypes.Value{}, errors.New("Unrecognized type: " + t.String()) 63 | } 64 | } 65 | 66 | // FromTerraform5Value accepts a tftypes.Value and returns our representation 67 | // of a Value. 68 | // 69 | // Note, that unlike the reverse ToTerraform5Value function we do not need to 70 | // include the type information as this is not embedded in our representation of 71 | // the type (the expectation is that the type information will always be 72 | // provided by the SDK regardless of which direction we need to go). 73 | func FromTerraform5Value(v tftypes.Value) (Value, error) { 74 | t := v.Type() 75 | switch { 76 | case t.Is(tftypes.Bool): 77 | ret := Value{} 78 | err := v.As(&ret.Boolean) 79 | return ret, err 80 | case t.Is(tftypes.String): 81 | ret := Value{} 82 | err := v.As(&ret.String) 83 | return ret, err 84 | case t.Is(tftypes.Number): 85 | ret := Value{} 86 | err := v.As(&ret.Number) 87 | return ret, err 88 | case t.Is(tftypes.List{}): 89 | return listFromTerraform5Value(v) 90 | case t.Is(tftypes.Map{}): 91 | return mapFromTerraform5Value(v) 92 | case t.Is(tftypes.Object{}): 93 | return objectFromTerraform5Value(v) 94 | case t.Is(tftypes.Set{}): 95 | return setFromTerraform5Value(v) 96 | default: 97 | return Value{}, errors.New("Unrecognized type: " + t.String()) 98 | } 99 | } 100 | 101 | func listToTerraform5Value(values *[]Value, listType tftypes.List) (tftypes.Value, error) { 102 | if values == nil { 103 | return tftypes.NewValue(listType, nil), nil 104 | } 105 | 106 | children := make([]tftypes.Value, 0) 107 | for _, value := range *values { 108 | child, err := ToTerraform5Value(value, listType.ElementType) 109 | if err != nil { 110 | return tftypes.Value{}, err 111 | } 112 | children = append(children, child) 113 | } 114 | return tftypes.NewValue(listType, children), nil 115 | } 116 | 117 | func mapToTerraform5Value(values *map[string]Value, mapType tftypes.Map) (tftypes.Value, error) { 118 | if values == nil { 119 | return tftypes.NewValue(mapType, nil), nil 120 | } 121 | 122 | children := make(map[string]tftypes.Value) 123 | for name, value := range *values { 124 | child, err := ToTerraform5Value(value, mapType.ElementType) 125 | if err != nil { 126 | return tftypes.Value{}, err 127 | } 128 | children[name] = child 129 | } 130 | return tftypes.NewValue(mapType, children), nil 131 | } 132 | 133 | // objectToTerraform5Value is a bit of a special case as we return the inner 134 | // value of the value instead of the value directly (as with the other 135 | // functions). This is because we use this function as part of our 136 | // implementation of the ValueCreator and ValueConverter of the Resource type 137 | // which expects the underlying structure instead of being already converted. 138 | func objectToTerraform5Value(values *map[string]Value, objectType tftypes.Object) (interface{}, error) { 139 | if values == nil { 140 | return nil, nil 141 | } 142 | 143 | children := make(map[string]tftypes.Value) 144 | for name, childType := range objectType.AttributeTypes { 145 | 146 | // It is possible that this child type exists in the type representation 147 | // but not in the actual value (this is because we can have optional 148 | // attributes in objects). So we try and retrieve the child from the 149 | // values but if it is not there we don't fail, instead we just set an 150 | // empty value in its place. 151 | 152 | var err error 153 | if value, ok := (*values)[name]; ok { 154 | if children[name], err = ToTerraform5Value(value, childType); err != nil { 155 | return nil, err 156 | } 157 | continue 158 | } 159 | 160 | // Otherwise we just set a nil value. 161 | children[name] = tftypes.NewValue(childType, nil) 162 | } 163 | return children, nil 164 | } 165 | 166 | func setToTerraform5Value(values *[]Value, setType tftypes.Set) (tftypes.Value, error) { 167 | if values == nil { 168 | return tftypes.NewValue(setType, nil), nil 169 | } 170 | 171 | children := make([]tftypes.Value, 0) 172 | for _, value := range *values { 173 | child, err := ToTerraform5Value(value, setType.ElementType) 174 | if err != nil { 175 | return tftypes.Value{}, err 176 | } 177 | children = append(children, child) 178 | } 179 | return tftypes.NewValue(setType, children), nil 180 | } 181 | 182 | func listFromTerraform5Value(v tftypes.Value) (Value, error) { 183 | var children []tftypes.Value 184 | if err := v.As(&children); err != nil { 185 | return Value{}, err 186 | } 187 | 188 | // There is a difference between a list being empty and being null from 189 | // Terraform's perspective. So we want to create a list of length 0 rather 190 | // than leaving it as null. 191 | list := make([]Value, 0) 192 | for _, child := range children { 193 | parsed, err := FromTerraform5Value(child) 194 | if err != nil { 195 | return Value{}, err 196 | } 197 | list = append(list, parsed) 198 | } 199 | 200 | return Value{ 201 | List: &list, 202 | }, nil 203 | } 204 | 205 | func mapFromTerraform5Value(v tftypes.Value) (Value, error) { 206 | var children map[string]tftypes.Value 207 | if err := v.As(&children); err != nil { 208 | return Value{}, err 209 | } 210 | 211 | values := make(map[string]Value) 212 | for name, child := range children { 213 | parsed, err := FromTerraform5Value(child) 214 | if err != nil { 215 | return Value{}, err 216 | } 217 | values[name] = parsed 218 | } 219 | 220 | return Value{ 221 | Map: &values, 222 | }, nil 223 | } 224 | 225 | func objectFromTerraform5Value(v tftypes.Value) (Value, error) { 226 | var children map[string]tftypes.Value 227 | if err := v.As(&children); err != nil { 228 | return Value{}, err 229 | } 230 | 231 | values := make(map[string]Value) 232 | for name, child := range children { 233 | if child.IsNull() || !child.IsKnown() { 234 | // Terraform handles unset objects differently to us. We just don't 235 | // add unset attributes to our objects while terraform adds them 236 | // but sets them to null. If this child value is null in the 237 | // Terraform representation we just skip it. 238 | // 239 | // Note, the reverse implementation in objectToTerrafrom5Value. We 240 | // check the type information and set any missing attributes as null 241 | // when converting into the terraform representation. 242 | // 243 | // For now, we also treat unknown values the same as null by just 244 | // skipping them. Any computed values will be filled in later. 245 | continue 246 | } 247 | 248 | parsed, err := FromTerraform5Value(child) 249 | if err != nil { 250 | return Value{}, err 251 | } 252 | values[name] = parsed 253 | 254 | } 255 | 256 | return Value{ 257 | Object: &values, 258 | }, nil 259 | } 260 | 261 | func setFromTerraform5Value(v tftypes.Value) (Value, error) { 262 | var children []tftypes.Value 263 | if err := v.As(&children); err != nil { 264 | return Value{}, err 265 | } 266 | 267 | set := make([]Value, 0) 268 | for _, child := range children { 269 | parsed, err := FromTerraform5Value(child) 270 | if err != nil { 271 | return Value{}, err 272 | } 273 | set = append(set, parsed) 274 | } 275 | 276 | return Value{ 277 | Set: &set, 278 | }, nil 279 | } 280 | -------------------------------------------------------------------------------- /internal/schema/action.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/hashicorp/terraform-plugin-framework/action/schema" 5 | "github.com/hashicorp/terraform-plugin-framework/attr" 6 | ) 7 | 8 | var ( 9 | actions = &AttributeTypes[schema.Attribute]{} 10 | ) 11 | 12 | func init() { 13 | actions.asBoolean = asActionBool 14 | actions.asFloat = asActionFloat 15 | actions.asInteger = asActionInteger 16 | actions.asNumber = asActionNumber 17 | actions.asString = asActionString 18 | actions.asList = asActionList 19 | actions.asNestedList = asActionNestedList 20 | actions.asMap = asActionMap 21 | actions.asNestedMap = asActionNestedMap 22 | actions.asSet = asActionSet 23 | actions.asNestedSet = asActionNestedSet 24 | actions.asObject = asActionObject 25 | actions.asNestedObject = asActionNestedObject 26 | } 27 | 28 | func asActionBool(attribute Attribute) (*schema.Attribute, error) { 29 | // action schemas don't have computed, but we share this definition with 30 | // resources and data sources. therefore, we set optional to true if the 31 | // attribute is computed. 32 | 33 | tfAttribute := schema.BoolAttribute{ 34 | Required: attribute.Required, 35 | Optional: attribute.Optional || attribute.Computed, 36 | Description: attribute.Description, 37 | MarkdownDescription: attribute.MarkdownDescription, 38 | } 39 | 40 | var out schema.Attribute 41 | out = tfAttribute 42 | return &out, nil 43 | } 44 | 45 | func asActionFloat(attribute Attribute) (*schema.Attribute, error) { 46 | // action schemas don't have computed, but we share this definition with 47 | // resources and data sources. therefore, we set optional to true if the 48 | // attribute is computed. 49 | 50 | tfAttribute := schema.Float64Attribute{ 51 | Required: attribute.Required, 52 | Optional: attribute.Optional || attribute.Computed, 53 | Description: attribute.Description, 54 | MarkdownDescription: attribute.MarkdownDescription, 55 | } 56 | 57 | var out schema.Attribute 58 | out = tfAttribute 59 | return &out, nil 60 | } 61 | 62 | func asActionInteger(attribute Attribute) (*schema.Attribute, error) { 63 | // action schemas don't have computed, but we share this definition with 64 | // resources and data sources. therefore, we set optional to true if the 65 | // attribute is computed. 66 | 67 | tfAttribute := schema.Int64Attribute{ 68 | Required: attribute.Required, 69 | Optional: attribute.Optional || attribute.Computed, 70 | Description: attribute.Description, 71 | MarkdownDescription: attribute.MarkdownDescription, 72 | } 73 | 74 | var out schema.Attribute 75 | out = tfAttribute 76 | return &out, nil 77 | } 78 | 79 | func asActionNumber(attribute Attribute) (*schema.Attribute, error) { 80 | // action schemas don't have computed, but we share this definition with 81 | // resources and data sources. therefore, we set optional to true if the 82 | // attribute is computed. 83 | 84 | tfAttribute := schema.NumberAttribute{ 85 | Required: attribute.Required, 86 | Optional: attribute.Optional || attribute.Computed, 87 | Description: attribute.Description, 88 | MarkdownDescription: attribute.MarkdownDescription, 89 | } 90 | 91 | var out schema.Attribute 92 | out = tfAttribute 93 | return &out, nil 94 | } 95 | 96 | func asActionString(attribute Attribute) (*schema.Attribute, error) { 97 | // action schemas don't have computed, but we share this definition with 98 | // resources and data sources. therefore, we set optional to true if the 99 | // attribute is computed. 100 | 101 | tfAttribute := schema.StringAttribute{ 102 | Required: attribute.Required, 103 | Optional: attribute.Optional || attribute.Computed, 104 | Description: attribute.Description, 105 | MarkdownDescription: attribute.MarkdownDescription, 106 | } 107 | 108 | var out schema.Attribute 109 | out = tfAttribute 110 | return &out, nil 111 | } 112 | 113 | func asActionList(attribute Attribute) (*schema.Attribute, error) { 114 | // action schemas don't have computed, but we share this definition with 115 | // resources and data sources. therefore, we set optional to true if the 116 | // attribute is computed. 117 | 118 | tfAttribute := schema.ListAttribute{ 119 | Required: attribute.Required, 120 | Optional: attribute.Optional || attribute.Computed, 121 | Description: attribute.Description, 122 | MarkdownDescription: attribute.MarkdownDescription, 123 | } 124 | 125 | elem, err := ToTerraformAttribute(*attribute.List, actions) 126 | if err != nil { 127 | return nil, err 128 | } 129 | tfAttribute.ElementType = (*elem).GetType() 130 | 131 | var out schema.Attribute 132 | out = tfAttribute 133 | return &out, nil 134 | } 135 | 136 | func asActionNestedList(attribute Attribute) (*schema.Attribute, error) { 137 | // action schemas don't have computed, but we share this definition with 138 | // resources and data sources. therefore, we set optional to true if the 139 | // attribute is computed. 140 | 141 | tfAttribute := schema.ListNestedAttribute{ 142 | Required: attribute.Required, 143 | Optional: attribute.Optional || attribute.Computed, 144 | Description: attribute.Description, 145 | MarkdownDescription: attribute.MarkdownDescription, 146 | } 147 | 148 | var err error 149 | if tfAttribute.NestedObject.Attributes, err = attributesToTerraformActionAttributes(attribute.List.Object); err != nil { 150 | return nil, err 151 | } 152 | 153 | var out schema.Attribute 154 | out = tfAttribute 155 | return &out, nil 156 | } 157 | 158 | func asActionMap(attribute Attribute) (*schema.Attribute, error) { 159 | // action schemas don't have computed, but we share this definition with 160 | // resources and data sources. therefore, we set optional to true if the 161 | // attribute is computed. 162 | 163 | tfAttribute := schema.MapAttribute{ 164 | Required: attribute.Required, 165 | Optional: attribute.Optional || attribute.Computed, 166 | Description: attribute.Description, 167 | MarkdownDescription: attribute.MarkdownDescription, 168 | } 169 | 170 | elem, err := ToTerraformAttribute(*attribute.Map, actions) 171 | if err != nil { 172 | return nil, err 173 | } 174 | tfAttribute.ElementType = (*elem).GetType() 175 | 176 | var out schema.Attribute 177 | out = tfAttribute 178 | return &out, nil 179 | } 180 | 181 | func asActionNestedMap(attribute Attribute) (*schema.Attribute, error) { 182 | // action schemas don't have computed, but we share this definition with 183 | // resources and data sources. therefore, we set optional to true if the 184 | // attribute is computed. 185 | 186 | tfAttribute := schema.MapNestedAttribute{ 187 | Required: attribute.Required, 188 | Optional: attribute.Optional || attribute.Computed, 189 | Description: attribute.Description, 190 | MarkdownDescription: attribute.MarkdownDescription, 191 | } 192 | 193 | var err error 194 | if tfAttribute.NestedObject.Attributes, err = attributesToTerraformActionAttributes(attribute.Map.Object); err != nil { 195 | return nil, err 196 | } 197 | 198 | var out schema.Attribute 199 | out = tfAttribute 200 | return &out, nil 201 | } 202 | 203 | func asActionSet(attribute Attribute) (*schema.Attribute, error) { 204 | // action schemas don't have computed, but we share this definition with 205 | // resources and data sources. therefore, we set optional to true if the 206 | // attribute is computed. 207 | 208 | tfAttribute := schema.SetAttribute{ 209 | Required: attribute.Required, 210 | Optional: attribute.Optional || attribute.Computed, 211 | Description: attribute.Description, 212 | MarkdownDescription: attribute.MarkdownDescription, 213 | } 214 | 215 | elem, err := ToTerraformAttribute(*attribute.Set, actions) 216 | if err != nil { 217 | return nil, err 218 | } 219 | tfAttribute.ElementType = (*elem).GetType() 220 | 221 | var out schema.Attribute 222 | out = tfAttribute 223 | return &out, nil 224 | } 225 | 226 | func asActionNestedSet(attribute Attribute) (*schema.Attribute, error) { 227 | // action schemas don't have computed, but we share this definition with 228 | // resources and data sources. therefore, we set optional to true if the 229 | // attribute is computed. 230 | 231 | tfAttribute := schema.SetNestedAttribute{ 232 | Required: attribute.Required, 233 | Optional: attribute.Optional || attribute.Computed, 234 | Description: attribute.Description, 235 | MarkdownDescription: attribute.MarkdownDescription, 236 | } 237 | 238 | var err error 239 | if tfAttribute.NestedObject.Attributes, err = attributesToTerraformActionAttributes(attribute.Set.Object); err != nil { 240 | return nil, err 241 | } 242 | 243 | var out schema.Attribute 244 | out = tfAttribute 245 | return &out, nil 246 | } 247 | 248 | func asActionObject(attribute Attribute) (*schema.Attribute, error) { 249 | // action schemas don't have computed, but we share this definition with 250 | // resources and data sources. therefore, we set optional to true if the 251 | // attribute is computed. 252 | 253 | tfAttribute := schema.ObjectAttribute{ 254 | Required: attribute.Required, 255 | Optional: attribute.Optional || attribute.Computed, 256 | Description: attribute.Description, 257 | MarkdownDescription: attribute.MarkdownDescription, 258 | } 259 | 260 | types := make(map[string]attr.Type) 261 | attributes, err := attributesToTerraformActionAttributes(attribute.Object) 262 | if err != nil { 263 | return nil, err 264 | } 265 | for key, value := range attributes { 266 | types[key] = value.GetType() 267 | } 268 | tfAttribute.AttributeTypes = types 269 | 270 | var out schema.Attribute 271 | out = tfAttribute 272 | return &out, nil 273 | } 274 | 275 | func asActionNestedObject(attribute Attribute) (*schema.Attribute, error) { 276 | // action schemas don't have computed, but we share this definition with 277 | // resources and data sources. therefore, we set optional to true if the 278 | // attribute is computed. 279 | 280 | tfAttribute := schema.SingleNestedAttribute{ 281 | Required: attribute.Required, 282 | Optional: attribute.Optional || attribute.Computed, 283 | Description: attribute.Description, 284 | MarkdownDescription: attribute.MarkdownDescription, 285 | } 286 | 287 | var err error 288 | if tfAttribute.Attributes, err = attributesToTerraformActionAttributes(attribute.Object); err != nil { 289 | return nil, err 290 | } 291 | 292 | var out schema.Attribute 293 | out = tfAttribute 294 | return &out, nil 295 | } 296 | -------------------------------------------------------------------------------- /internal/schema/resource.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package schema 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-framework/attr" 8 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 9 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" 10 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/float64planmodifier" 11 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" 12 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" 13 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" 14 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/numberplanmodifier" 15 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" 16 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" 17 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 18 | ) 19 | 20 | var ( 21 | resources = &AttributeTypes[schema.Attribute]{} 22 | ) 23 | 24 | func init() { 25 | resources.asBoolean = asResourceBool 26 | resources.asFloat = asResourceFloat 27 | resources.asInteger = asResourceInteger 28 | resources.asNumber = asResourceNumber 29 | resources.asString = asResourceString 30 | resources.asList = asResourceList 31 | resources.asNestedList = asResourceNestedList 32 | resources.asMap = asResourceMap 33 | resources.asNestedMap = asResourceNestedMap 34 | resources.asSet = asResourceSet 35 | resources.asNestedSet = asResourceNestedSet 36 | resources.asObject = asResourceObject 37 | resources.asNestedObject = asResourceNestedObject 38 | } 39 | 40 | func asResourceBool(attribute Attribute) (*schema.Attribute, error) { 41 | tfAttribute := schema.BoolAttribute{ 42 | Description: attribute.Description, 43 | MarkdownDescription: attribute.MarkdownDescription, 44 | Optional: attribute.Optional, 45 | Required: attribute.Required, 46 | Computed: attribute.Computed, 47 | Sensitive: attribute.Sensitive, 48 | } 49 | 50 | if attribute.Computed { 51 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, boolplanmodifier.UseStateForUnknown()) 52 | } 53 | 54 | if attribute.Replace { 55 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, boolplanmodifier.RequiresReplace()) 56 | } 57 | 58 | var out schema.Attribute 59 | out = tfAttribute 60 | return &out, nil 61 | } 62 | 63 | func asResourceFloat(attribute Attribute) (*schema.Attribute, error) { 64 | tfAttribute := schema.Float64Attribute{ 65 | Description: attribute.Description, 66 | MarkdownDescription: attribute.MarkdownDescription, 67 | Optional: attribute.Optional, 68 | Required: attribute.Required, 69 | Computed: attribute.Computed, 70 | Sensitive: attribute.Sensitive, 71 | } 72 | 73 | if attribute.Computed { 74 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, float64planmodifier.UseStateForUnknown()) 75 | } 76 | 77 | if attribute.Replace { 78 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, float64planmodifier.RequiresReplace()) 79 | } 80 | 81 | var out schema.Attribute 82 | out = tfAttribute 83 | return &out, nil 84 | } 85 | 86 | func asResourceInteger(attribute Attribute) (*schema.Attribute, error) { 87 | tfAttribute := schema.Int64Attribute{ 88 | Description: attribute.Description, 89 | MarkdownDescription: attribute.MarkdownDescription, 90 | Optional: attribute.Optional, 91 | Required: attribute.Required, 92 | Computed: attribute.Computed, 93 | Sensitive: attribute.Sensitive, 94 | } 95 | 96 | if attribute.Computed { 97 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, int64planmodifier.UseStateForUnknown()) 98 | } 99 | 100 | if attribute.Replace { 101 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, int64planmodifier.RequiresReplace()) 102 | } 103 | 104 | var out schema.Attribute 105 | out = tfAttribute 106 | return &out, nil 107 | } 108 | 109 | func asResourceNumber(attribute Attribute) (*schema.Attribute, error) { 110 | tfAttribute := schema.NumberAttribute{ 111 | Description: attribute.Description, 112 | MarkdownDescription: attribute.MarkdownDescription, 113 | Optional: attribute.Optional, 114 | Required: attribute.Required, 115 | Computed: attribute.Computed, 116 | Sensitive: attribute.Sensitive, 117 | } 118 | 119 | if attribute.Computed { 120 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, numberplanmodifier.UseStateForUnknown()) 121 | } 122 | 123 | if attribute.Replace { 124 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, numberplanmodifier.RequiresReplace()) 125 | } 126 | 127 | var out schema.Attribute 128 | out = tfAttribute 129 | return &out, nil 130 | } 131 | 132 | func asResourceString(attribute Attribute) (*schema.Attribute, error) { 133 | tfAttribute := schema.StringAttribute{ 134 | Description: attribute.Description, 135 | MarkdownDescription: attribute.MarkdownDescription, 136 | Optional: attribute.Optional, 137 | Required: attribute.Required, 138 | Computed: attribute.Computed, 139 | Sensitive: attribute.Sensitive, 140 | } 141 | 142 | if attribute.Computed { 143 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, stringplanmodifier.UseStateForUnknown()) 144 | } 145 | 146 | if attribute.Replace { 147 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, stringplanmodifier.RequiresReplace()) 148 | } 149 | 150 | var out schema.Attribute 151 | out = tfAttribute 152 | return &out, nil 153 | } 154 | 155 | func asResourceList(attribute Attribute) (*schema.Attribute, error) { 156 | tfAttribute := schema.ListAttribute{ 157 | Description: attribute.Description, 158 | MarkdownDescription: attribute.MarkdownDescription, 159 | Optional: attribute.Optional, 160 | Required: attribute.Required, 161 | Computed: attribute.Computed, 162 | Sensitive: attribute.Sensitive, 163 | } 164 | 165 | elem, err := ToTerraformAttribute(*attribute.List, resources) 166 | if err != nil { 167 | return nil, err 168 | } 169 | tfAttribute.ElementType = (*elem).GetType() 170 | 171 | if attribute.Computed { 172 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, listplanmodifier.UseStateForUnknown()) 173 | } 174 | 175 | if attribute.Replace { 176 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, listplanmodifier.RequiresReplace()) 177 | } 178 | 179 | var out schema.Attribute 180 | out = tfAttribute 181 | return &out, nil 182 | } 183 | 184 | func asResourceNestedList(attribute Attribute) (*schema.Attribute, error) { 185 | tfAttribute := schema.ListNestedAttribute{ 186 | Description: attribute.Description, 187 | MarkdownDescription: attribute.MarkdownDescription, 188 | Optional: attribute.Optional, 189 | Required: attribute.Required, 190 | Computed: attribute.Computed, 191 | Sensitive: attribute.Sensitive, 192 | } 193 | 194 | var err error 195 | if tfAttribute.NestedObject.Attributes, err = attributesToTerraformResourceAttributes(attribute.List.Object); err != nil { 196 | return nil, err 197 | } 198 | 199 | if attribute.Computed { 200 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, listplanmodifier.UseStateForUnknown()) 201 | } 202 | 203 | if attribute.Replace { 204 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, listplanmodifier.RequiresReplace()) 205 | } 206 | 207 | var out schema.Attribute 208 | out = tfAttribute 209 | return &out, nil 210 | } 211 | 212 | func asResourceMap(attribute Attribute) (*schema.Attribute, error) { 213 | tfAttribute := schema.MapAttribute{ 214 | Description: attribute.Description, 215 | MarkdownDescription: attribute.MarkdownDescription, 216 | Optional: attribute.Optional, 217 | Required: attribute.Required, 218 | Computed: attribute.Computed, 219 | Sensitive: attribute.Sensitive, 220 | } 221 | 222 | elem, err := ToTerraformAttribute(*attribute.Map, resources) 223 | if err != nil { 224 | return nil, err 225 | } 226 | tfAttribute.ElementType = (*elem).GetType() 227 | 228 | if attribute.Computed { 229 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, mapplanmodifier.UseStateForUnknown()) 230 | } 231 | 232 | if attribute.Replace { 233 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, mapplanmodifier.RequiresReplace()) 234 | } 235 | 236 | var out schema.Attribute 237 | out = tfAttribute 238 | return &out, nil 239 | } 240 | 241 | func asResourceNestedMap(attribute Attribute) (*schema.Attribute, error) { 242 | tfAttribute := schema.MapNestedAttribute{ 243 | Description: attribute.Description, 244 | MarkdownDescription: attribute.MarkdownDescription, 245 | Optional: attribute.Optional, 246 | Required: attribute.Required, 247 | Computed: attribute.Computed, 248 | Sensitive: attribute.Sensitive, 249 | } 250 | 251 | var err error 252 | if tfAttribute.NestedObject.Attributes, err = attributesToTerraformResourceAttributes(attribute.Map.Object); err != nil { 253 | return nil, err 254 | } 255 | 256 | if attribute.Computed { 257 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, mapplanmodifier.UseStateForUnknown()) 258 | } 259 | 260 | if attribute.Replace { 261 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, mapplanmodifier.RequiresReplace()) 262 | } 263 | 264 | var out schema.Attribute 265 | out = tfAttribute 266 | return &out, nil 267 | } 268 | 269 | func asResourceSet(attribute Attribute) (*schema.Attribute, error) { 270 | tfAttribute := schema.SetAttribute{ 271 | Description: attribute.Description, 272 | MarkdownDescription: attribute.MarkdownDescription, 273 | Optional: attribute.Optional, 274 | Required: attribute.Required, 275 | Computed: attribute.Computed, 276 | Sensitive: attribute.Sensitive, 277 | } 278 | 279 | elem, err := ToTerraformAttribute(*attribute.Set, resources) 280 | if err != nil { 281 | return nil, err 282 | } 283 | tfAttribute.ElementType = (*elem).GetType() 284 | 285 | if attribute.Computed { 286 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, setplanmodifier.UseStateForUnknown()) 287 | } 288 | 289 | if attribute.Replace { 290 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, setplanmodifier.RequiresReplace()) 291 | } 292 | 293 | var out schema.Attribute 294 | out = tfAttribute 295 | return &out, nil 296 | } 297 | 298 | func asResourceNestedSet(attribute Attribute) (*schema.Attribute, error) { 299 | tfAttribute := schema.SetNestedAttribute{ 300 | Description: attribute.Description, 301 | MarkdownDescription: attribute.MarkdownDescription, 302 | Optional: attribute.Optional, 303 | Required: attribute.Required, 304 | Computed: attribute.Computed, 305 | Sensitive: attribute.Sensitive, 306 | } 307 | 308 | var err error 309 | if tfAttribute.NestedObject.Attributes, err = attributesToTerraformResourceAttributes(attribute.Set.Object); err != nil { 310 | return nil, err 311 | } 312 | 313 | if attribute.Computed { 314 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, setplanmodifier.UseStateForUnknown()) 315 | } 316 | 317 | if attribute.Replace { 318 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, setplanmodifier.RequiresReplace()) 319 | } 320 | 321 | var out schema.Attribute 322 | out = tfAttribute 323 | return &out, nil 324 | } 325 | 326 | func asResourceObject(attribute Attribute) (*schema.Attribute, error) { 327 | tfAttribute := schema.ObjectAttribute{ 328 | Description: attribute.Description, 329 | MarkdownDescription: attribute.MarkdownDescription, 330 | Optional: attribute.Optional, 331 | Required: attribute.Required, 332 | Computed: attribute.Computed, 333 | Sensitive: attribute.Sensitive, 334 | } 335 | 336 | types := make(map[string]attr.Type) 337 | attributes, err := attributesToTerraformResourceAttributes(attribute.Object) 338 | if err != nil { 339 | return nil, err 340 | } 341 | for key, value := range attributes { 342 | types[key] = value.GetType() 343 | } 344 | tfAttribute.AttributeTypes = types 345 | 346 | if attribute.Computed { 347 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, objectplanmodifier.UseStateForUnknown()) 348 | } 349 | 350 | if attribute.Replace { 351 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, objectplanmodifier.RequiresReplace()) 352 | } 353 | 354 | var out schema.Attribute 355 | out = tfAttribute 356 | return &out, nil 357 | } 358 | 359 | func asResourceNestedObject(attribute Attribute) (*schema.Attribute, error) { 360 | tfAttribute := schema.SingleNestedAttribute{ 361 | Description: attribute.Description, 362 | MarkdownDescription: attribute.MarkdownDescription, 363 | Optional: attribute.Optional, 364 | Required: attribute.Required, 365 | Computed: attribute.Computed, 366 | Sensitive: attribute.Sensitive, 367 | } 368 | 369 | var err error 370 | if tfAttribute.Attributes, err = attributesToTerraformResourceAttributes(attribute.Object); err != nil { 371 | return nil, err 372 | } 373 | 374 | if attribute.Computed { 375 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, objectplanmodifier.UseStateForUnknown()) 376 | } 377 | 378 | if attribute.Replace { 379 | tfAttribute.PlanModifiers = append(tfAttribute.PlanModifiers, objectplanmodifier.RequiresReplace()) 380 | } 381 | 382 | var out schema.Attribute 383 | out = tfAttribute 384 | return &out, nil 385 | } 386 | --------------------------------------------------------------------------------