From a804c92a8b36f0482d2f3472b254bdc2ae0bf45b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 10:54:11 -0500 Subject: [PATCH 01/53] build(deps): bump github.com/hashicorp/copywrite in /tools (#1429) Bumps [github.com/hashicorp/copywrite](https://github.com/hashicorp/copywrite) from 0.20.0 to 0.21.0. - [Release notes](https://github.com/hashicorp/copywrite/releases) - [Changelog](https://github.com/hashicorp/copywrite/blob/main/.goreleaser.yaml) - [Commits](https://github.com/hashicorp/copywrite/compare/v0.20.0...v0.21.0) --- updated-dependencies: - dependency-name: github.com/hashicorp/copywrite dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tools/go.mod | 2 +- tools/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/go.mod b/tools/go.mod index 4dbb34ee456..1e0f5946cf0 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -2,7 +2,7 @@ module tools go 1.22.7 -require github.com/hashicorp/copywrite v0.20.0 +require github.com/hashicorp/copywrite v0.21.0 require ( github.com/AlecAivazis/survey/v2 v2.3.7 // indirect diff --git a/tools/go.sum b/tools/go.sum index a889a22a8a6..fcb732348a6 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -144,8 +144,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= -github.com/hashicorp/copywrite v0.20.0 h1:i+iNq4lWsGopKIhC0HfZjUvNAnXnU/Pc5e+4L5WF+1Y= -github.com/hashicorp/copywrite v0.20.0/go.mod h1:mu6DAyUI6m6vq8weoJn9a0HDuUUrV+0GQdRp4mD50yU= +github.com/hashicorp/copywrite v0.21.0 h1:IE8uByQdos8s0uAyHF4O8RHV5cJEhmIc+Awk+wkKXKI= +github.com/hashicorp/copywrite v0.21.0/go.mod h1:mu6DAyUI6m6vq8weoJn9a0HDuUUrV+0GQdRp4mD50yU= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= From 301ea9f1730c8c8b414f2ebb53cbd3efd52b2aef Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 26 Feb 2025 11:47:10 -0500 Subject: [PATCH 02/53] Add `max-issues-per-linter` (#1434) --- .golangci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.golangci.yml b/.golangci.yml index 3e4a04f3642..d3d2390bf27 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -3,7 +3,7 @@ issues: - linters: - staticcheck text: 'SA1019: schema.SchemaValidateFunc is deprecated' - max-per-linter: 0 + max-issues-per-linter: 0 max-same-issues: 0 linters: From 3c4ee107f7a0e565bf0fa9aaadfbf619eb11df58 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Mon, 3 Mar 2025 09:17:30 -0500 Subject: [PATCH 03/53] Add callout for `PreferWriteOnlyAttribute` validator (#1436) --- helper/validation/write_only.go | 4 ++++ .../docs/plugin/sdkv2/resources/write-only-arguments.mdx | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/helper/validation/write_only.go b/helper/validation/write_only.go index 303e72dd2d1..84418ef170e 100644 --- a/helper/validation/write_only.go +++ b/helper/validation/write_only.go @@ -22,6 +22,10 @@ import ( // For lists: cty.Index(cty.UnknownVal(cty.Number)), // For maps: cty.Index(cty.UnknownVal(cty.String)), // For sets: cty.Index(cty.UnknownVal(cty.Object(nil))), +// +// NOTE: This validator will produce persistent warnings for practitioners on every Terraform run as long as the specified non-write-only attribute +// has a value in the configuration. The validator will also produce warnings for users of shared modules +// who cannot immediately take action on the warning. func PreferWriteOnlyAttribute(oldAttribute cty.Path, writeOnlyAttribute cty.Path) schema.ValidateRawResourceConfigFunc { return func(ctx context.Context, req schema.ValidateResourceConfigFuncRequest, resp *schema.ValidateResourceConfigFuncResponse) { if !req.WriteOnlyAttributesAllowed { diff --git a/website/docs/plugin/sdkv2/resources/write-only-arguments.mdx b/website/docs/plugin/sdkv2/resources/write-only-arguments.mdx index f4a82ad436c..add53209ec4 100644 --- a/website/docs/plugin/sdkv2/resources/write-only-arguments.mdx +++ b/website/docs/plugin/sdkv2/resources/write-only-arguments.mdx @@ -203,6 +203,13 @@ if !woVal.IsNull() { ## PreferWriteOnlyAttribute Validator + + + This validator will produce persistent warnings for practitioners on every Terraform run as long as the specified non-write-only attribute + has a value in the configuration. The validator will also produce warnings for users of shared modules who cannot immediately take action on the warning. + + + `PreferWriteOnlyAttribute()` is a validator that takes a `cty.Path` to an existing configuration attribute (required/optional) and a `cty.Path` to a write-only argument. Use this validator when you have a write-only version of an existing attribute, and you want to encourage practitioners to use the write-only version whenever possible. From e492918c5a2fd84702dfdc596b5316f3cd3d36df Mon Sep 17 00:00:00 2001 From: Leah Bush <157434496+LeahMarieBush@users.noreply.github.com> Date: Thu, 6 Mar 2025 12:31:18 -0600 Subject: [PATCH 04/53] Add comments for UDR migration (#1440) --- website/README.md | 3 +++ website/docs/plugin/sdkv2/best-practices/deprecations.mdx | 3 +++ website/docs/plugin/sdkv2/best-practices/detecting-drift.mdx | 3 +++ website/docs/plugin/sdkv2/best-practices/index.mdx | 3 +++ website/docs/plugin/sdkv2/debugging.mdx | 3 +++ .../docs/plugin/sdkv2/guides/terraform-0.12-compatibility.mdx | 3 +++ website/docs/plugin/sdkv2/guides/v1-upgrade-guide.mdx | 3 +++ website/docs/plugin/sdkv2/guides/v2-upgrade-guide.mdx | 3 +++ website/docs/plugin/sdkv2/index.mdx | 3 +++ website/docs/plugin/sdkv2/logging/http-transport.mdx | 3 +++ website/docs/plugin/sdkv2/logging/index.mdx | 3 +++ .../docs/plugin/sdkv2/resources/customizing-differences.mdx | 3 +++ .../docs/plugin/sdkv2/resources/data-consistency-errors.mdx | 3 +++ website/docs/plugin/sdkv2/resources/import.mdx | 3 +++ website/docs/plugin/sdkv2/resources/index.mdx | 3 +++ .../sdkv2/resources/retries-and-customizable-timeouts.mdx | 3 +++ website/docs/plugin/sdkv2/resources/state-migration.mdx | 3 +++ website/docs/plugin/sdkv2/resources/write-only-arguments.mdx | 3 +++ website/docs/plugin/sdkv2/schemas/index.mdx | 3 +++ website/docs/plugin/sdkv2/schemas/schema-behaviors.mdx | 3 +++ website/docs/plugin/sdkv2/schemas/schema-methods.mdx | 3 +++ website/docs/plugin/sdkv2/schemas/schema-types.mdx | 3 +++ website/docs/plugin/sdkv2/testing/acceptance-tests/index.mdx | 3 +++ .../docs/plugin/sdkv2/testing/acceptance-tests/sweepers.mdx | 3 +++ .../docs/plugin/sdkv2/testing/acceptance-tests/testcase.mdx | 3 +++ .../docs/plugin/sdkv2/testing/acceptance-tests/teststep.mdx | 3 +++ website/docs/plugin/sdkv2/testing/index.mdx | 3 +++ website/docs/plugin/sdkv2/testing/testing-api.mdx | 3 +++ website/docs/plugin/sdkv2/testing/testing-patterns.mdx | 3 +++ website/docs/plugin/sdkv2/testing/unit-testing.mdx | 3 +++ 30 files changed, 90 insertions(+) diff --git a/website/README.md b/website/README.md index 03473e00623..8d1e314f14b 100644 --- a/website/README.md +++ b/website/README.md @@ -1,5 +1,8 @@ # Terraform Documentation +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + This directory contains the portions of [the Terraform website][terraform.io] that pertain to the Terraform Plugin SDK. The files in this directory are intended to be used in conjunction with diff --git a/website/docs/plugin/sdkv2/best-practices/deprecations.mdx b/website/docs/plugin/sdkv2/best-practices/deprecations.mdx index 3499ad9fa74..e9309db140a 100644 --- a/website/docs/plugin/sdkv2/best-practices/deprecations.mdx +++ b/website/docs/plugin/sdkv2/best-practices/deprecations.mdx @@ -3,6 +3,9 @@ page_title: 'Plugin Development - SDKv2 Deprecations, Removals, and Renames Best description: 'Recommendations for deprecations, removals, and renames.' --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Deprecations, Removals, and Renames Terraform is trusted for managing many facets of infrastructure across many organizations. Part of that trust is due to consistent versioning guidelines and setting expectations for various levels of upgrades. Ensuring backwards compatibility for all patch and minor releases, potentially in concert with any upcoming major changes, is recommended and supported by the Terraform development framework. This allows operators to iteratively update their Terraform configurations rather than require massive refactoring. diff --git a/website/docs/plugin/sdkv2/best-practices/detecting-drift.mdx b/website/docs/plugin/sdkv2/best-practices/detecting-drift.mdx index 762c6347022..0e1f6df0cb8 100644 --- a/website/docs/plugin/sdkv2/best-practices/detecting-drift.mdx +++ b/website/docs/plugin/sdkv2/best-practices/detecting-drift.mdx @@ -6,6 +6,9 @@ description: |- infrastructure has changed. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Detecting Drift One of the core challenges of infrastructure as code is keeping an up-to-date diff --git a/website/docs/plugin/sdkv2/best-practices/index.mdx b/website/docs/plugin/sdkv2/best-practices/index.mdx index 85d6b2d44e4..1b5463df3a0 100644 --- a/website/docs/plugin/sdkv2/best-practices/index.mdx +++ b/website/docs/plugin/sdkv2/best-practices/index.mdx @@ -4,6 +4,9 @@ description: >- Patterns that ensure a consistent user experience, including deprecation, beta features, and detecting drift. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + This best practices section only contains guidance for plugins built with [Terraform Plugin SDK](/terraform/plugin/sdkv2). More generic best practices that apply to both SDK and [Terraform Plugin Framework](/terraform/plugin/framework) can be found in the [Plugin Development Best Practices](/terraform/plugin/best-practices) section. diff --git a/website/docs/plugin/sdkv2/debugging.mdx b/website/docs/plugin/sdkv2/debugging.mdx index 22341e0df43..3e17d31f2f2 100644 --- a/website/docs/plugin/sdkv2/debugging.mdx +++ b/website/docs/plugin/sdkv2/debugging.mdx @@ -3,6 +3,9 @@ page_title: Plugin Development - Debugging SDKv2 Providers description: How to implement debugger support in SDKv2 Terraform providers. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Debugging SDKv2 Providers This page contains implementation details for inspecting runtime information of a Terraform provider developed with SDKv2 via a debugger tool. Review the top level [Debugging](/terraform/plugin/debugging) page for information pertaining to the overall Terraform provider debugging process and other inspection options, such as log-based debugging. diff --git a/website/docs/plugin/sdkv2/guides/terraform-0.12-compatibility.mdx b/website/docs/plugin/sdkv2/guides/terraform-0.12-compatibility.mdx index 442ffdb76ad..709cde63211 100644 --- a/website/docs/plugin/sdkv2/guides/terraform-0.12-compatibility.mdx +++ b/website/docs/plugin/sdkv2/guides/terraform-0.12-compatibility.mdx @@ -5,6 +5,9 @@ description: |- codebases. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Terraform 0.12 Compatibility for Providers Terraform 0.12 introduced a new type system for the Terraform language, and diff --git a/website/docs/plugin/sdkv2/guides/v1-upgrade-guide.mdx b/website/docs/plugin/sdkv2/guides/v1-upgrade-guide.mdx index fc5e8d99a59..6c31944315e 100644 --- a/website/docs/plugin/sdkv2/guides/v1-upgrade-guide.mdx +++ b/website/docs/plugin/sdkv2/guides/v1-upgrade-guide.mdx @@ -3,6 +3,9 @@ page_title: Terraform Plugin SDK description: Official standalone SDK for Terraform plugin development --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Terraform Plugin SDK As of September 2019, Terraform provider developers importing the Go module `github.com/hashicorp/terraform`, known as Terraform Core, should switch to `github.com/hashicorp/terraform-plugin-sdk`, the Terraform Plugin SDK, instead. diff --git a/website/docs/plugin/sdkv2/guides/v2-upgrade-guide.mdx b/website/docs/plugin/sdkv2/guides/v2-upgrade-guide.mdx index f34ad78fa36..9119c4b814a 100644 --- a/website/docs/plugin/sdkv2/guides/v2-upgrade-guide.mdx +++ b/website/docs/plugin/sdkv2/guides/v2-upgrade-guide.mdx @@ -3,6 +3,9 @@ page_title: Terraform Plugin SDK v2 Upgrade Guide description: Upgrade guide for version 2 of the Terraform Plugin SDK. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Terraform Plugin SDK v2 Upgrade Guide Version 2.0.0 of the Terraform Plugin SDK is a major release and includes some diff --git a/website/docs/plugin/sdkv2/index.mdx b/website/docs/plugin/sdkv2/index.mdx index 8947e80b065..fc6d7cdad3d 100644 --- a/website/docs/plugin/sdkv2/index.mdx +++ b/website/docs/plugin/sdkv2/index.mdx @@ -3,6 +3,9 @@ page_title: 'Home - Plugin Development: SDKv2' description: Maintain plugins built on the legacy SDK. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Terraform Plugin SDKv2 Terraform Plugin SDKv2 is a way to maintain Terraform Plugins on [protocol version 5](/terraform/plugin/terraform-plugin-protocol#protocol-version-5). diff --git a/website/docs/plugin/sdkv2/logging/http-transport.mdx b/website/docs/plugin/sdkv2/logging/http-transport.mdx index 4302b6d6405..5800f5e0a51 100644 --- a/website/docs/plugin/sdkv2/logging/http-transport.mdx +++ b/website/docs/plugin/sdkv2/logging/http-transport.mdx @@ -4,6 +4,9 @@ description: |- SDKv2 provides a helper to send all the HTTP transactions to structured logging. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # HTTP Transport Terraform's public interface has included `helper/logging` [`NewTransport()`](https://github.com/hashicorp/terraform-plugin-sdk/blob/main/helper/logging/transport.go) since `v0.9.5`. This helper is an implementation of the Golang standard library [`http.RoundTripper`](https://pkg.go.dev/net/http#RoundTripper) that lets you add logging at the `DEBUG` level to your provider's HTTP transactions. diff --git a/website/docs/plugin/sdkv2/logging/index.mdx b/website/docs/plugin/sdkv2/logging/index.mdx index 9cfc333c6fa..7542619a891 100644 --- a/website/docs/plugin/sdkv2/logging/index.mdx +++ b/website/docs/plugin/sdkv2/logging/index.mdx @@ -4,6 +4,9 @@ description: |- High-quality logs are important when debugging your provider. Learn to set-up logging and write meaningful logs. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Logging Terraform Plugin SDKv2 integrates with the structured logging framework [terraform-plugin-log](/terraform/plugin/log). High-quality logs are critical to quickly [debugging your provider](/terraform/plugin/debugging). diff --git a/website/docs/plugin/sdkv2/resources/customizing-differences.mdx b/website/docs/plugin/sdkv2/resources/customizing-differences.mdx index b04afa21d4d..b8ad606ab7b 100644 --- a/website/docs/plugin/sdkv2/resources/customizing-differences.mdx +++ b/website/docs/plugin/sdkv2/resources/customizing-differences.mdx @@ -3,6 +3,9 @@ page_title: Resources - Customizing Differences description: Difference customization within Resources. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Resources - Customizing Differences Terraform tracks the state of provisioned resources in its state file, and compares the user-passed configuration against that state. When Terraform detects a discrepancy, it presents the user with the differences between the configuration and the state. diff --git a/website/docs/plugin/sdkv2/resources/data-consistency-errors.mdx b/website/docs/plugin/sdkv2/resources/data-consistency-errors.mdx index bb50e1564a0..0197ef84256 100644 --- a/website/docs/plugin/sdkv2/resources/data-consistency-errors.mdx +++ b/website/docs/plugin/sdkv2/resources/data-consistency-errors.mdx @@ -3,6 +3,9 @@ page_title: Resources - Data Consistency Errors description: Fixing data consistency errors caused by this SDK. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Resources - Data Consistency Errors Resources written with `terraform-plugin-sdk` are by default allowed to perform unexpected data handling operations from Terraform's perspective. Terraform versions 0.12 and later have stricter [data consistency rules](https://github.com/hashicorp/terraform/blob/main/docs/resource-instance-change-lifecycle.md) than the design and implementation of this SDK, which predated those rules. diff --git a/website/docs/plugin/sdkv2/resources/import.mdx b/website/docs/plugin/sdkv2/resources/import.mdx index 7b2e1c51755..a56a8872b6c 100644 --- a/website/docs/plugin/sdkv2/resources/import.mdx +++ b/website/docs/plugin/sdkv2/resources/import.mdx @@ -3,6 +3,9 @@ page_title: Resources - Import description: Implementing resource import support. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Resources - Import Adding import support for Terraform resources will allow existing infrastructure to be managed within Terraform. This type of enhancement generally requires a small to moderate amount of code changes. diff --git a/website/docs/plugin/sdkv2/resources/index.mdx b/website/docs/plugin/sdkv2/resources/index.mdx index 127148e9683..e2fd9f02726 100644 --- a/website/docs/plugin/sdkv2/resources/index.mdx +++ b/website/docs/plugin/sdkv2/resources/index.mdx @@ -5,6 +5,9 @@ description: >- resource APIs. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Resources A key component to Terraform Provider development is defining the creation, read, update, and deletion functionality of a resource to map those API operations into the Terraform lifecycle. While the [Call APIs with Terraform Providers tutorial](/terraform/tutorials/providers?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) and [Schemas documentation](/terraform/plugin/sdkv2/schemas) cover the basic aspects of developing Terraform resources, this section covers more advanced features of resource development. diff --git a/website/docs/plugin/sdkv2/resources/retries-and-customizable-timeouts.mdx b/website/docs/plugin/sdkv2/resources/retries-and-customizable-timeouts.mdx index 985ad53b60a..fc7f729b5d6 100644 --- a/website/docs/plugin/sdkv2/resources/retries-and-customizable-timeouts.mdx +++ b/website/docs/plugin/sdkv2/resources/retries-and-customizable-timeouts.mdx @@ -3,6 +3,9 @@ page_title: Resources - Retries and Customizable Timeouts description: Helpers for handling retries within Resources. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Resources - Retries and Customizable Timeouts The reality of cloud infrastructure is that it typically takes time to perform operations such as booting operating systems, discovering services, and replicating state across network edges. As the provider developer you should take known delays in resource APIs into account in the CRUD functions of the resource. Terraform supports configurable timeouts to assist in these situations. diff --git a/website/docs/plugin/sdkv2/resources/state-migration.mdx b/website/docs/plugin/sdkv2/resources/state-migration.mdx index 689fc26e949..fcc7f1c42d0 100644 --- a/website/docs/plugin/sdkv2/resources/state-migration.mdx +++ b/website/docs/plugin/sdkv2/resources/state-migration.mdx @@ -3,6 +3,9 @@ page_title: Resources - State Migration description: Migrating state values within resources. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Resources - State Migration Resources define the data types and API interactions required to create, update, and destroy infrastructure with a cloud vendor while the [Terraform state](/terraform/language/state) stores mapping and metadata information for those remote objects. There are several reasons why a resource implementation needs to change: backend APIs Terraform interacts with will change overtime, or the current implementation might be incorrect or unmaintainable. Some of these changes may not be backward compatible and a migration is needed for resources provisioned in the wild with old schema configurations. diff --git a/website/docs/plugin/sdkv2/resources/write-only-arguments.mdx b/website/docs/plugin/sdkv2/resources/write-only-arguments.mdx index add53209ec4..5848c2045b4 100644 --- a/website/docs/plugin/sdkv2/resources/write-only-arguments.mdx +++ b/website/docs/plugin/sdkv2/resources/write-only-arguments.mdx @@ -3,6 +3,9 @@ page_title: Resources - Write-only Arguments description: Implementing write-only arguments within resources. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Resources - Write-only Arguments ~> **NOTE:** Write-only arguments are only supported in Terraform `v1.11` or higher diff --git a/website/docs/plugin/sdkv2/schemas/index.mdx b/website/docs/plugin/sdkv2/schemas/index.mdx index 6a00e371b2d..36f92963dc1 100644 --- a/website/docs/plugin/sdkv2/schemas/index.mdx +++ b/website/docs/plugin/sdkv2/schemas/index.mdx @@ -5,6 +5,9 @@ description: |- in SDKv2. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Terraform Schemas Terraform Plugins are expressed using schemas to define attributes and their diff --git a/website/docs/plugin/sdkv2/schemas/schema-behaviors.mdx b/website/docs/plugin/sdkv2/schemas/schema-behaviors.mdx index 30ac5262d9e..5a215213256 100644 --- a/website/docs/plugin/sdkv2/schemas/schema-behaviors.mdx +++ b/website/docs/plugin/sdkv2/schemas/schema-behaviors.mdx @@ -5,6 +5,9 @@ description: |- you can use to define element behaviors in SDKv2. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Schema Behaviors Schema fields that can have an effect at plan or apply time are collectively diff --git a/website/docs/plugin/sdkv2/schemas/schema-methods.mdx b/website/docs/plugin/sdkv2/schemas/schema-methods.mdx index 7d56b8bf9df..083bc097473 100644 --- a/website/docs/plugin/sdkv2/schemas/schema-methods.mdx +++ b/website/docs/plugin/sdkv2/schemas/schema-methods.mdx @@ -5,6 +5,9 @@ description: |- to extend Terraform's core offering. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Terraform Schemas Methods _NOTE_ should distinguish between `schema.Provider`, `schema.Resource`, diff --git a/website/docs/plugin/sdkv2/schemas/schema-types.mdx b/website/docs/plugin/sdkv2/schemas/schema-types.mdx index 9003cbd0640..a087c0a61ce 100644 --- a/website/docs/plugin/sdkv2/schemas/schema-types.mdx +++ b/website/docs/plugin/sdkv2/schemas/schema-types.mdx @@ -6,6 +6,9 @@ description: |- element. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Schema Attributes and Types Almost every Terraform Plugin offers user configurable parameters, examples such diff --git a/website/docs/plugin/sdkv2/testing/acceptance-tests/index.mdx b/website/docs/plugin/sdkv2/testing/acceptance-tests/index.mdx index f0d2cecb22d..de5e1ab66dc 100644 --- a/website/docs/plugin/sdkv2/testing/acceptance-tests/index.mdx +++ b/website/docs/plugin/sdkv2/testing/acceptance-tests/index.mdx @@ -5,6 +5,9 @@ description: |- imitate applying one or more configuration files. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Acceptance Tests diff --git a/website/docs/plugin/sdkv2/testing/acceptance-tests/sweepers.mdx b/website/docs/plugin/sdkv2/testing/acceptance-tests/sweepers.mdx index ddcceaf3a30..f17f7593268 100644 --- a/website/docs/plugin/sdkv2/testing/acceptance-tests/sweepers.mdx +++ b/website/docs/plugin/sdkv2/testing/acceptance-tests/sweepers.mdx @@ -5,6 +5,9 @@ description: >- testing framework. Sweepers clean up leftover infrastructure. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Sweepers diff --git a/website/docs/plugin/sdkv2/testing/acceptance-tests/testcase.mdx b/website/docs/plugin/sdkv2/testing/acceptance-tests/testcase.mdx index d44b2852345..629a91068b1 100644 --- a/website/docs/plugin/sdkv2/testing/acceptance-tests/testcase.mdx +++ b/website/docs/plugin/sdkv2/testing/acceptance-tests/testcase.mdx @@ -5,6 +5,9 @@ description: |- creates a set of resources then verifies the new infrastructure. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Acceptance Tests: TestCases diff --git a/website/docs/plugin/sdkv2/testing/acceptance-tests/teststep.mdx b/website/docs/plugin/sdkv2/testing/acceptance-tests/teststep.mdx index 6f696d326e2..6fe69ac5b4a 100644 --- a/website/docs/plugin/sdkv2/testing/acceptance-tests/teststep.mdx +++ b/website/docs/plugin/sdkv2/testing/acceptance-tests/teststep.mdx @@ -5,6 +5,9 @@ description: |- file to a given state. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Acceptance Tests: TestSteps diff --git a/website/docs/plugin/sdkv2/testing/index.mdx b/website/docs/plugin/sdkv2/testing/index.mdx index f92e3d86ae5..4d6e3eb0a5c 100644 --- a/website/docs/plugin/sdkv2/testing/index.mdx +++ b/website/docs/plugin/sdkv2/testing/index.mdx @@ -5,6 +5,9 @@ description: |- plugins. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Testing Terraform Plugins diff --git a/website/docs/plugin/sdkv2/testing/testing-api.mdx b/website/docs/plugin/sdkv2/testing/testing-api.mdx index de0a59bd9f1..f38c5daa8d4 100644 --- a/website/docs/plugin/sdkv2/testing/testing-api.mdx +++ b/website/docs/plugin/sdkv2/testing/testing-api.mdx @@ -5,4 +5,7 @@ description: |- to extend Terraform's core offering. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Testing API diff --git a/website/docs/plugin/sdkv2/testing/testing-patterns.mdx b/website/docs/plugin/sdkv2/testing/testing-patterns.mdx index d68074cca2f..a704e0095a9 100644 --- a/website/docs/plugin/sdkv2/testing/testing-patterns.mdx +++ b/website/docs/plugin/sdkv2/testing/testing-patterns.mdx @@ -5,4 +5,7 @@ description: |- to extend Terraform's core offering. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Testing Patterns diff --git a/website/docs/plugin/sdkv2/testing/unit-testing.mdx b/website/docs/plugin/sdkv2/testing/unit-testing.mdx index ea69c174e44..ce59c93a49b 100644 --- a/website/docs/plugin/sdkv2/testing/unit-testing.mdx +++ b/website/docs/plugin/sdkv2/testing/unit-testing.mdx @@ -5,6 +5,9 @@ description: |- flatten API responses into data structures that Terraform stores as state. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Unit Testing From be03b72fb3198f025b9b5a3ba1fe7f69a8e6ece8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Mar 2025 15:06:08 -0500 Subject: [PATCH 05/53] build(deps): bump github.com/hashicorp/go-cty (#1439) Bumps [github.com/hashicorp/go-cty](https://github.com/hashicorp/go-cty) from 1.4.1-0.20200414143053-d3edf31b6320 to 1.4.1. - [Changelog](https://github.com/hashicorp/go-cty/blob/master/CHANGELOG.md) - [Commits](https://github.com/hashicorp/go-cty/commits/v1.4.1) --- updated-dependencies: - dependency-name: github.com/hashicorp/go-cty dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 4745e1e61b9..54ed8aa0226 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.22.7 require ( github.com/google/go-cmp v0.6.0 - github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 + github.com/hashicorp/go-cty v1.4.1 github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-plugin v1.6.2 github.com/hashicorp/go-uuid v1.0.3 diff --git a/go.sum b/go.sum index 4c9606b27d1..980deb69deb 100644 --- a/go.sum +++ b/go.sum @@ -55,8 +55,8 @@ github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuD github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI= -github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= +github.com/hashicorp/go-cty v1.4.1 h1:T4i4kbEKuyMoe4Ujh52Ud07VXr05dnP/Si9JiVDpx3Y= +github.com/hashicorp/go-cty v1.4.1/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= From 3105146c5c35b47f60aa579bdcbef10fc2c85f2a Mon Sep 17 00:00:00 2001 From: "hashicorp-tsccr[bot]" <129506189+hashicorp-tsccr[bot]@users.noreply.github.com> Date: Fri, 7 Mar 2025 15:22:45 -0500 Subject: [PATCH 06/53] Result of tsccr-helper -log-level=info gha update -latest .github/ (#1430) Co-authored-by: hashicorp-tsccr[bot] --- .github/workflows/ci-go.yml | 2 +- .github/workflows/ci-goreleaser.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index ed10a887ddf..b8ab11469dc 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -21,7 +21,7 @@ jobs: with: go-version-file: 'go.mod' - run: go mod download - - uses: golangci/golangci-lint-action@2e788936b09dd82dc280e845628a40d2ba6b204c # v6.3.1 + - uses: golangci/golangci-lint-action@2226d7cb06a077cd73e56eedd38eecad18e5d837 # v6.5.0 with: version: latest terraform-provider-corner-tfprotov5: diff --git a/.github/workflows/ci-goreleaser.yml b/.github/workflows/ci-goreleaser.yml index 2c62e130b60..76735ffb5a2 100644 --- a/.github/workflows/ci-goreleaser.yml +++ b/.github/workflows/ci-goreleaser.yml @@ -18,6 +18,6 @@ jobs: - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version-file: 'go.mod' - - uses: goreleaser/goreleaser-action@026299872805cb2db698e02dd7fb506a4da5122d # v6.2.0 + - uses: goreleaser/goreleaser-action@90a3faa9d0182683851fbfa97ca1a2cb983bfca3 # v6.2.1 with: args: check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 802a636b5da..b59bd7cdd31 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -124,7 +124,7 @@ jobs: cd .changes sed -e "1{/# /d;}" -e "2{/^$/d;}" ${{ needs.changelog-version.outputs.version }}.md > /tmp/release-notes.txt - - uses: goreleaser/goreleaser-action@026299872805cb2db698e02dd7fb506a4da5122d # v6.2.0 + - uses: goreleaser/goreleaser-action@90a3faa9d0182683851fbfa97ca1a2cb983bfca3 # v6.2.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: From 9533d95dee7edc783593a37dd8deaa8224cd6952 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Mar 2025 16:16:28 -0500 Subject: [PATCH 07/53] build(deps): bump github.com/google/go-cmp from 0.6.0 to 0.7.0 (#1432) Bumps [github.com/google/go-cmp](https://github.com/google/go-cmp) from 0.6.0 to 0.7.0. - [Release notes](https://github.com/google/go-cmp/releases) - [Commits](https://github.com/google/go-cmp/compare/v0.6.0...v0.7.0) --- updated-dependencies: - dependency-name: github.com/google/go-cmp dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 54ed8aa0226..221142ea61b 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22.0 toolchain go1.22.7 require ( - github.com/google/go-cmp v0.6.0 + github.com/google/go-cmp v0.7.0 github.com/hashicorp/go-cty v1.4.1 github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-plugin v1.6.2 diff --git a/go.sum b/go.sum index 980deb69deb..383ac6ede65 100644 --- a/go.sum +++ b/go.sum @@ -44,8 +44,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= From 264f8360651aca75f0ba8aa52ceb37ff7433038f Mon Sep 17 00:00:00 2001 From: "hashicorp-tsccr[bot]" <129506189+hashicorp-tsccr[bot]@users.noreply.github.com> Date: Mon, 10 Mar 2025 11:21:35 +0100 Subject: [PATCH 08/53] Result of tsccr-helper -log-level=info gha update -latest .github/ (#1441) Co-authored-by: hashicorp-tsccr[bot] --- .github/workflows/ci-go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index b8ab11469dc..43bfd0c5aee 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -66,7 +66,7 @@ jobs: - run: go mod download - run: go test -coverprofile=coverage.out ./... - run: go tool cover -html=coverage.out -o coverage.html - - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: go-${{ matrix.go-version }}-coverage path: coverage.html From b1817aa76d9e9b8756d2d2da374113ac68504415 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Mon, 17 Mar 2025 15:11:14 -0400 Subject: [PATCH 09/53] Update hashicorp/go-cty to v1.5.0 (#1437) --- go.mod | 2 +- go.sum | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 221142ea61b..fed6706716f 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.22.7 require ( github.com/google/go-cmp v0.7.0 - github.com/hashicorp/go-cty v1.4.1 + github.com/hashicorp/go-cty v1.5.0 github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-plugin v1.6.2 github.com/hashicorp/go-uuid v1.0.3 diff --git a/go.sum b/go.sum index 383ac6ede65..236e3037175 100644 --- a/go.sum +++ b/go.sum @@ -55,8 +55,8 @@ github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuD github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-cty v1.4.1 h1:T4i4kbEKuyMoe4Ujh52Ud07VXr05dnP/Si9JiVDpx3Y= -github.com/hashicorp/go-cty v1.4.1/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= +github.com/hashicorp/go-cty v1.5.0 h1:EkQ/v+dDNUqnuVpmS5fPqyY71NXVgT5gf32+57xY8g0= +github.com/hashicorp/go-cty v1.5.0/go.mod h1:lFUCG5kd8exDobgSfyj4ONE/dc822kiYMguVKdHGMLM= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= @@ -195,7 +195,6 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= From f304deb2075ecef8c23e8561b8367c5f85fb8dcc Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 18 Mar 2025 09:04:05 -0400 Subject: [PATCH 10/53] chore: Update minimum Go version in module (#1445) * chore: Update minimum Go version in module * add changelogs --- .../unreleased/NOTES-20250317-152749.yaml | 7 ++++++ .../NOTES-20250317-152749.yaml | 7 ++++++ .copywrite.hcl | 2 +- .github/workflows/ci-go.yml | 2 +- README.md | 2 +- go.mod | 14 +++++------ go.sum | 24 +++++++++---------- tools/go.mod | 14 +++++------ tools/go.sum | 24 +++++++++---------- 9 files changed, 55 insertions(+), 41 deletions(-) create mode 100644 .changes/unreleased/NOTES-20250317-152749.yaml create mode 100644 .changes/unreleased/upcoming-stable/NOTES-20250317-152749.yaml diff --git a/.changes/unreleased/NOTES-20250317-152749.yaml b/.changes/unreleased/NOTES-20250317-152749.yaml new file mode 100644 index 00000000000..d9faaebe4ab --- /dev/null +++ b/.changes/unreleased/NOTES-20250317-152749.yaml @@ -0,0 +1,7 @@ +kind: NOTES +body: 'all: This Go module has been updated to Go 1.23 per the [Go support policy](https://go.dev/doc/devel/release#policy). + It is recommended to review the [Go 1.23 release notes](https://go.dev/doc/go1.23) + before upgrading. Any consumers building on earlier Go versions may experience errors.' +time: 2025-03-17T15:27:49.758739-04:00 +custom: + Issue: "1445" diff --git a/.changes/unreleased/upcoming-stable/NOTES-20250317-152749.yaml b/.changes/unreleased/upcoming-stable/NOTES-20250317-152749.yaml new file mode 100644 index 00000000000..d9faaebe4ab --- /dev/null +++ b/.changes/unreleased/upcoming-stable/NOTES-20250317-152749.yaml @@ -0,0 +1,7 @@ +kind: NOTES +body: 'all: This Go module has been updated to Go 1.23 per the [Go support policy](https://go.dev/doc/devel/release#policy). + It is recommended to review the [Go 1.23 release notes](https://go.dev/doc/go1.23) + before upgrading. Any consumers building on earlier Go versions may experience errors.' +time: 2025-03-17T15:27:49.758739-04:00 +custom: + Issue: "1445" diff --git a/.copywrite.hcl b/.copywrite.hcl index 22700a3d3f7..d755ab73b18 100644 --- a/.copywrite.hcl +++ b/.copywrite.hcl @@ -6,7 +6,7 @@ project { header_ignore = [ # changie tooling configuration and CHANGELOG entries (prose) - ".changes/unreleased/*.yaml", + ".changes/unreleased/**", ".changie.yaml", # GitHub issue template configuration diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index 43bfd0c5aee..43629cd3484 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -57,7 +57,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: [ '1.23', '1.22' ] + go-version: [ '1.24', '1.23' ] steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 diff --git a/README.md b/README.md index bc3474078dc..9ea49cef353 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ When running provider tests, Terraform 0.12.26 or later is needed for version 2. This project follows the [support policy](https://golang.org/doc/devel/release.html#policy) of Go as its support policy. The two latest major releases of Go are supported by the project. -Currently, that means Go **1.22** or later must be used when including this project as a dependency. +Currently, that means Go **1.23** or later must be used when including this project as a dependency. ## Getting Started diff --git a/go.mod b/go.mod index fed6706716f..885b616ff10 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/hashicorp/terraform-plugin-sdk/v2 -go 1.22.0 +go 1.23.0 -toolchain go1.22.7 +toolchain go1.23.7 require ( github.com/google/go-cmp v0.7.0 @@ -23,7 +23,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/reflectwalk v1.0.2 github.com/zclconf/go-cty v1.16.2 - golang.org/x/crypto v0.33.0 + golang.org/x/crypto v0.36.0 ) require ( @@ -49,10 +49,10 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect golang.org/x/mod v0.22.0 // indirect - golang.org/x/net v0.34.0 // indirect - golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.22.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect diff --git a/go.sum b/go.sum index 236e3037175..2915f261526 100644 --- a/go.sum +++ b/go.sum @@ -160,8 +160,8 @@ go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HY go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= @@ -169,13 +169,13 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -188,18 +188,18 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= -golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/tools/go.mod b/tools/go.mod index 1e0f5946cf0..938dd04b733 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -1,6 +1,6 @@ module tools -go 1.22.7 +go 1.23.7 require github.com/hashicorp/copywrite v0.21.0 @@ -47,14 +47,14 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/thanhpk/randstr v1.0.4 // indirect go.mongodb.org/mongo-driver v1.10.0 // indirect - golang.org/x/crypto v0.31.0 // indirect + golang.org/x/crypto v0.36.0 // indirect golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect - golang.org/x/net v0.33.0 // indirect + golang.org/x/net v0.37.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/term v0.27.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/tools/go.sum b/tools/go.sum index fcb732348a6..abe7c9dc7f1 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -372,8 +372,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= @@ -411,8 +411,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -429,8 +429,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -477,16 +477,16 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -498,8 +498,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From 53dccde4cb0229253a079969e05cc9088ffb81ef Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 18 Mar 2025 16:12:06 -0400 Subject: [PATCH 11/53] chore: Add `prerelease: auto` for future pre-releases (#1446) --- .goreleaser.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.goreleaser.yml b/.goreleaser.yml index cb189e494c4..e1c9a0df571 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -5,5 +5,6 @@ builds: milestones: - close: true release: + prerelease: auto ids: - 'none' From 02b0b3b0366d6e57b599029da7e43e567c53a86c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Mar 2025 09:45:29 -0400 Subject: [PATCH 12/53] build(deps): bump github.com/hashicorp/copywrite in /tools (#1448) Bumps [github.com/hashicorp/copywrite](https://github.com/hashicorp/copywrite) from 0.21.0 to 0.22.0. - [Release notes](https://github.com/hashicorp/copywrite/releases) - [Changelog](https://github.com/hashicorp/copywrite/blob/main/.goreleaser.yaml) - [Commits](https://github.com/hashicorp/copywrite/compare/v0.21.0...v0.22.0) --- updated-dependencies: - dependency-name: github.com/hashicorp/copywrite dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tools/go.mod | 2 +- tools/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/go.mod b/tools/go.mod index 938dd04b733..7ba09d85002 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -2,7 +2,7 @@ module tools go 1.23.7 -require github.com/hashicorp/copywrite v0.21.0 +require github.com/hashicorp/copywrite v0.22.0 require ( github.com/AlecAivazis/survey/v2 v2.3.7 // indirect diff --git a/tools/go.sum b/tools/go.sum index abe7c9dc7f1..f5e51f9d857 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -144,8 +144,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= -github.com/hashicorp/copywrite v0.21.0 h1:IE8uByQdos8s0uAyHF4O8RHV5cJEhmIc+Awk+wkKXKI= -github.com/hashicorp/copywrite v0.21.0/go.mod h1:mu6DAyUI6m6vq8weoJn9a0HDuUUrV+0GQdRp4mD50yU= +github.com/hashicorp/copywrite v0.22.0 h1:mqjMrgP3VptS7aLbu2l39rtznoK+BhphHst6i7HiTAo= +github.com/hashicorp/copywrite v0.22.0/go.mod h1:FqvGJt2+yoYDpVYgFSdg3R2iyhkCVaBmPMhfso0MR2k= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= From f58eb0000a21005f1d18a75a06bb8d25a7df44b8 Mon Sep 17 00:00:00 2001 From: Ansgar Mertens Date: Wed, 19 Mar 2025 15:24:10 +0100 Subject: [PATCH 13/53] Resource Identity: Add identity data to existing RPCs and add two new identity related RPCs" (#1444) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * update plugin go * initial generate of RPC + small shim test * wip - resource identity schema * Added initial testing for getresourceidentityschemas * Added initial attempt for GetResourceIdentitySchemas and UpgradeResourceIdentity. * start implementing identity in ReadResource – test doesn't work yet, but getting close * Added more for GetResourceIdentitySchemas and UpgradeResourceIdentity. * Copied CoreIdentitySchema to CoreResourceIdentitySchema and started ConfigIdentitySchemaToProto and coreConfigIdentitySchema to use in GetResourceIdentitySchemas * Continuing addition of functions for Resource Identity * Added TODO for internal validation later * Tests for GetResourceIdentity passing * Initial test written for UpgradeResourceIdentity passing * make ReadResource test pass * use the right identity upgraders * support get, set for identity data * support get, set for identity data in diffs in PlanResourceChange customize diff functions * Added to the internal validation in schema and corresponding tests, need to check if the expected behavior is correct * Added TestUpgradeResourceIdentity_removedAttr and TestUpgradeResourceIdentity_jsonStateBigInt however BigInt doesn't work just yet * safeguard empty identity sent by Terraform * implement identity in ApplyResourceChange * allow using Identity() when there wasn't one yet * fix check for previous state * Added scaffolding for internalIdentityValidate in resource.go to validate resource identities * update terraform-plugin-go to latest main * fix diff possibly being empty * update RawIdentity to RawState as the former was retired * update code comments and remove obsolete todos * address review feedback * fix lint * add copyright headers * fix TestUpgradeResourceIdentity_jsonStateBigInt test * update todo comments * remove obsolete comment * remove validation for identity schemas, will be done in separate PR --------- Co-authored-by: Austin Valle Co-authored-by: Rain --- go.mod | 10 +- go.sum | 42 +- helper/schema/core_schema.go | 22 + helper/schema/grpc_provider.go | 250 ++++++++- helper/schema/grpc_provider_test.go | 650 ++++++++++++++++++++++++ helper/schema/identity_data.go | 138 +++++ helper/schema/resource.go | 35 +- helper/schema/resource_data.go | 72 ++- helper/schema/resource_diff.go | 35 +- helper/schema/resource_diff_test.go | 150 +++--- helper/schema/resource_identity.go | 72 +++ helper/schema/schema.go | 146 +++++- helper/schema/schema_test.go | 6 +- helper/schema/shims.go | 6 +- internal/configs/configschema/schema.go | 10 + internal/plugin/convert/schema.go | 24 + terraform/diff.go | 7 +- terraform/state.go | 12 + 18 files changed, 1552 insertions(+), 135 deletions(-) create mode 100644 helper/schema/identity_data.go create mode 100644 helper/schema/resource_identity.go diff --git a/go.mod b/go.mod index 885b616ff10..92ee6894c37 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/google/go-cmp v0.7.0 github.com/hashicorp/go-cty v1.5.0 github.com/hashicorp/go-hclog v1.6.3 - github.com/hashicorp/go-plugin v1.6.2 + github.com/hashicorp/go-plugin v1.6.3 github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/hc-install v0.9.1 @@ -16,7 +16,7 @@ require ( github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-exec v0.22.0 github.com/hashicorp/terraform-json v0.24.0 - github.com/hashicorp/terraform-plugin-go v0.26.0 + github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250313132633-b799d5c93127 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/mitchellh/copystructure v1.2.0 github.com/mitchellh/go-testing-interface v1.14.1 @@ -55,7 +55,7 @@ require ( golang.org/x/text v0.23.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect - google.golang.org/grpc v1.69.4 // indirect - google.golang.org/protobuf v1.36.3 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect + google.golang.org/grpc v1.71.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect ) diff --git a/go.sum b/go.sum index 2915f261526..04cdc15a342 100644 --- a/go.sum +++ b/go.sum @@ -61,8 +61,8 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= -github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= +github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg= +github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -80,8 +80,8 @@ github.com/hashicorp/terraform-exec v0.22.0 h1:G5+4Sz6jYZfRYUCg6eQgDsqTzkNXV+fP8 github.com/hashicorp/terraform-exec v0.22.0/go.mod h1:bjVbsncaeh8jVdhttWYZuBGj21FcYw6Ia/XfHcNO7lQ= github.com/hashicorp/terraform-json v0.24.0 h1:rUiyF+x1kYawXeRth6fKFm/MdfBS6+lW4NbeATsYz8Q= github.com/hashicorp/terraform-json v0.24.0/go.mod h1:Nfj5ubo9xbu9uiAoZVBsNOjvNKB66Oyrvtit74kC7ow= -github.com/hashicorp/terraform-plugin-go v0.26.0 h1:cuIzCv4qwigug3OS7iKhpGAbZTiypAfFQmw8aE65O2M= -github.com/hashicorp/terraform-plugin-go v0.26.0/go.mod h1:+CXjuLDiFgqR+GcrM5a2E2Kal5t5q2jb0E3D57tTdNY= +github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250313132633-b799d5c93127 h1:V7RFaz+LWQY/S8tiWLuVFQMigEoZOpCF2vv7FW83vVw= +github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250313132633-b799d5c93127/go.mod h1:MfDwS/KnIy2QzCwdRtuqIjZ23gpYa9Vm+Z8cFpx8qtU= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.2.4 h1:JXu/zHB2Ymg/TGVCRu10XqNa4Sh2bWcqCNyKWjnCPJA= @@ -148,16 +148,18 @@ github.com/zclconf/go-cty v1.16.2 h1:LAJSwc3v81IRBZyUVQDUdZ7hs3SYs9jv0eZJDWHD/70 github.com/zclconf/go-cty v1.16.2/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= -go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= -go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= -go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= -go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= -go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= -go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= -go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= -go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= -go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= -go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= @@ -210,14 +212,14 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= -google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= -google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/helper/schema/core_schema.go b/helper/schema/core_schema.go index 9247adde7e4..e01ca497135 100644 --- a/helper/schema/core_schema.go +++ b/helper/schema/core_schema.go @@ -168,6 +168,9 @@ func (s *Schema) coreConfigSchemaAttribute() *configschema.Attribute { DescriptionKind: descKind, Deprecated: s.Deprecated != "", WriteOnly: s.WriteOnly, + // For Identity Attributes only + OptionalForImport: s.OptionalForImport, + RequiredForImport: s.RequiredForImport, } } @@ -370,3 +373,22 @@ func (r *Resource) CoreConfigSchema() *configschema.Block { func (r *Resource) coreConfigSchema() *configschema.Block { return schemaMap(r.SchemaMap()).CoreConfigSchema() } + +func (r *Resource) CoreIdentitySchema() *configschema.Block { + block := r.coreIdentitySchema() + + if block.Attributes == nil { + // TODO: we should error instead and callers should handle the error appropriately + // and error would hint at an invalid provider implementation + block.Attributes = map[string]*configschema.Attribute{} + } + + return block +} + +func (r *Resource) coreIdentitySchema() *configschema.Block { + // while there is schemaMapWithIdentity, we don't need to use it here + // as we're only interested in the existing CoreConfigSchema() method + // to convert our schema + return schemaMap(r.Identity.Schema).CoreConfigSchema() +} diff --git a/helper/schema/grpc_provider.go b/helper/schema/grpc_provider.go index f942814806c..0071d1c8d6b 100644 --- a/helper/schema/grpc_provider.go +++ b/helper/schema/grpc_provider.go @@ -77,6 +77,104 @@ func (s *GRPCProviderServer) serverCapabilities() *tfprotov5.ServerCapabilities } } +func (s *GRPCProviderServer) GetResourceIdentitySchemas(ctx context.Context, req *tfprotov5.GetResourceIdentitySchemasRequest) (*tfprotov5.GetResourceIdentitySchemasResponse, error) { + ctx = logging.InitContext(ctx) + + logging.HelperSchemaTrace(ctx, "Getting resource identity schemas") + + resp := &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: make(map[string]*tfprotov5.ResourceIdentitySchema), + } + + for typ, res := range s.provider.ResourcesMap { + logging.HelperSchemaTrace(ctx, "Found resource identity type", map[string]interface{}{logging.KeyResourceType: typ}) + + if res.Identity != nil { + resp.IdentitySchemas[typ] = &tfprotov5.ResourceIdentitySchema{ + Version: res.Identity.Version, + IdentityAttributes: convert.ConfigIdentitySchemaToProto(ctx, res.CoreIdentitySchema()), + } + } + } + + return resp, nil + +} + +func (s *GRPCProviderServer) UpgradeResourceIdentity(ctx context.Context, req *tfprotov5.UpgradeResourceIdentityRequest) (*tfprotov5.UpgradeResourceIdentityResponse, error) { + ctx = logging.InitContext(ctx) + resp := &tfprotov5.UpgradeResourceIdentityResponse{} + + res, ok := s.provider.ResourcesMap[req.TypeName] + if !ok { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("unknown resource type: %s", req.TypeName)) + return resp, nil + } + schemaBlock := s.getResourceIdentitySchemaBlock(req.TypeName) + + version := req.Version + + jsonMap := map[string]interface{}{} + var err error + + switch { + // if there's a JSON state, we need to decode it. + case req.RawIdentity != nil && len(req.RawIdentity.JSON) > 0: + if res.UseJSONNumber { + err = unmarshalJSON(req.RawIdentity.JSON, &jsonMap) + } else { + err = json.Unmarshal(req.RawIdentity.JSON, &jsonMap) + } + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) + return resp, nil + } + default: + logging.HelperSchemaDebug(ctx, "no resource identity provided to upgrade") + return resp, nil + } + + // complete the upgrade of the JSON states + logging.HelperSchemaTrace(ctx, "Upgrading JSON identity") + + jsonMap, err = s.upgradeJSONIdentity(ctx, version, jsonMap, res) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) + return resp, nil + } + + // The provider isn't required to clean out removed fields + s.removeAttributes(ctx, jsonMap, schemaBlock.ImpliedType()) + + // now we need to turn the state into the default json representation, so + // that it can be re-decoded using the actual schema. + val, err := JSONMapToStateValue(jsonMap, schemaBlock) // TODO: Find out if we need resource identity version here + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) + return resp, nil + } + + // Now we need to make sure blocks are represented correctly, which means + // that missing blocks are empty collections, rather than null. + // First we need to CoerceValue to ensure that all object types match. + val, err = schemaBlock.CoerceValue(val) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) + return resp, nil + } + + // encode the final state to the expected msgpack format + newStateMP, err := msgpack.Marshal(val, schemaBlock.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) + return resp, nil + } + + resp.UpgradedIdentity = &tfprotov5.ResourceIdentityData{IdentityData: &tfprotov5.DynamicValue{MsgPack: newStateMP}} + + return resp, nil +} + func (s *GRPCProviderServer) GetMetadata(ctx context.Context, req *tfprotov5.GetMetadataRequest) (*tfprotov5.GetMetadataResponse, error) { ctx = logging.InitContext(ctx) @@ -160,6 +258,11 @@ func (s *GRPCProviderServer) getResourceSchemaBlock(name string) *configschema.B return res.CoreConfigSchema() } +func (s *GRPCProviderServer) getResourceIdentitySchemaBlock(name string) *configschema.Block { + res := s.provider.ResourcesMap[name] + return res.CoreIdentitySchema() +} + func (s *GRPCProviderServer) getDatasourceSchemaBlock(name string) *configschema.Block { dat := s.provider.DataSourcesMap[name] return dat.CoreConfigSchema() @@ -698,6 +801,7 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re ) resp.NewState = req.CurrentState + resp.NewIdentity = req.CurrentIdentity resp.Deferred = &tfprotov5.Deferred{ Reason: tfprotov5.DeferredReason(s.provider.providerDeferred.Reason), } @@ -717,6 +821,23 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re } instanceState.RawState = stateVal + // TODO: is there a more elegant way to do this? this requires us to look for the identity schema block again + if req.CurrentIdentity != nil && req.CurrentIdentity.IdentityData != nil { + + // convert req.CurrentIdentity to flat map identity structure + // Step 1: Turn JSON into cty.Value based on schema + identityBlock := s.getResourceIdentitySchemaBlock(req.TypeName) // TODO: handle error if no schema exists (and after we add the error) + identityVal, err := msgpack.Unmarshal(req.CurrentIdentity.IdentityData.MsgPack, identityBlock.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) + return resp, nil + } + // Step 2: Turn cty.Value into flatmap representation + identityAttrs := hcl2shim.FlatmapValueFromHCL2(identityVal) + // Step 3: Well, set it in the instanceState + instanceState.Identity = identityAttrs + } + private := make(map[string]interface{}) if len(req.Private) > 0 { if err := json.Unmarshal(req.Private, &private); err != nil { @@ -779,6 +900,28 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re MsgPack: newStateMP, } + if newInstanceState.Identity != nil { + identityBlock := s.getResourceIdentitySchemaBlock(req.TypeName) // TODO: handle error if no schema exists (and after we add the error) + + newIdentityVal, err := hcl2shim.HCL2ValueFromFlatmap(newInstanceState.Identity, identityBlock.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) + return resp, nil + } + + newIdentityMP, err := msgpack.Marshal(newIdentityVal, identityBlock.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) + return resp, nil + } + + resp.NewIdentity = &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: newIdentityMP, + }, + } + } + return resp, nil } @@ -820,6 +963,7 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot resp.Deferred = &tfprotov5.Deferred{ Reason: tfprotov5.DeferredReason(s.provider.providerDeferred.Reason), } + resp.PlannedIdentity = req.PriorIdentity return resp, nil } @@ -841,6 +985,7 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot if proposedNewStateVal.IsNull() { resp.PlannedState = req.ProposedNewState resp.PlannedPrivate = req.PriorPrivate + resp.PlannedIdentity = req.PriorIdentity return resp, nil } @@ -887,6 +1032,22 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot // turn the proposed state into a legacy configuration cfg := terraform.NewResourceConfigShimmed(proposedNewStateVal, schemaBlock) + // add identity data to priorState + if req.PriorIdentity != nil && req.PriorIdentity.IdentityData != nil { + // convert req.PriorIdentity to flat map identity structure + // Step 1: Turn JSON into cty.Value based on schema + identityBlock := s.getResourceIdentitySchemaBlock(req.TypeName) // TODO: handle error if no schema exists (and after we add the error) + identityVal, err := msgpack.Unmarshal(req.PriorIdentity.IdentityData.MsgPack, identityBlock.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) + return resp, nil + } + // Step 2: Turn cty.Value into flatmap representation + identityAttrs := hcl2shim.FlatmapValueFromHCL2(identityVal) + // Step 3: Well, set it in the priorState + priorState.Identity = identityAttrs + } + diff, err := res.SimpleDiff(ctx, priorState, cfg, s.provider.Meta()) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) @@ -902,15 +1063,17 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot diff.Attributes["id"] = &terraform.ResourceAttrDiff{ NewComputed: true, } + // TODO: we could error here if a new Diff got no Identity set } - if diff == nil || len(diff.Attributes) == 0 { + if diff == nil || (len(diff.Attributes) == 0 && len(diff.Identity) == 0) { // schema.Provider.Diff returns nil if it ends up making a diff with no // changes, but our new interface wants us to return an actual change // description that _shows_ there are no changes. This is always the // prior state, because we force a diff above if this is a new instance. resp.PlannedState = req.PriorState resp.PlannedPrivate = req.PriorPrivate + resp.PlannedIdentity = req.PriorIdentity return resp, nil } @@ -1061,6 +1224,29 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot } } + // TODO: if schema defines identity, we should error if there's none written to newInstanceState + if res.Identity != nil { + identityBlock := s.getResourceIdentitySchemaBlock(req.TypeName) // TODO: handle error if no schema exists (and after we add the error) + + newIdentityVal, err := hcl2shim.HCL2ValueFromFlatmap(diff.Identity, identityBlock.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) + return resp, nil + } + + newIdentityMP, err := msgpack.Marshal(newIdentityVal, identityBlock.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) + return resp, nil + } + + resp.PlannedIdentity = &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: newIdentityMP, + }, + } + } + return resp, nil } @@ -1110,6 +1296,22 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro } } + // add identity data to priorState + if req.PlannedIdentity != nil && req.PlannedIdentity.IdentityData != nil { + // convert req.PriorIdentity to flat map identity structure + // Step 1: Turn JSON into cty.Value based on schema + identityBlock := s.getResourceIdentitySchemaBlock(req.TypeName) // TODO: handle error if no schema exists (and after we add the error) + identityVal, err := msgpack.Unmarshal(req.PlannedIdentity.IdentityData.MsgPack, identityBlock.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) + return resp, nil + } + // Step 2: Turn cty.Value into flatmap representation + identityAttrs := hcl2shim.FlatmapValueFromHCL2(identityVal) + // Step 3: Well, set it in the priorState + priorState.Identity = identityAttrs + } + var diff *terraform.InstanceDiff destroy := false @@ -1123,6 +1325,7 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro RawPlan: plannedStateVal, RawState: priorStateVal, RawConfig: configVal, + Identity: priorState.Identity, } } else { diff, err = DiffFromValues(ctx, priorStateVal, plannedStateVal, configVal, stripResourceModifiers(res)) @@ -1139,7 +1342,10 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro RawPlan: plannedStateVal, RawState: priorStateVal, RawConfig: configVal, + Identity: priorState.Identity, } + } else { + diff.Identity = priorState.Identity } // add NewExtra Fields that may have been stored in the private data @@ -1235,6 +1441,29 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro } resp.Private = meta + // TODO: if schema defines identity, we should error if there's none written to newInstanceState + if res.Identity != nil { + identityBlock := s.getResourceIdentitySchemaBlock(req.TypeName) // TODO: handle error if no schema exists (and after we add the error) + + newIdentityVal, err := hcl2shim.HCL2ValueFromFlatmap(newInstanceState.Identity, identityBlock.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) + return resp, nil + } + + newIdentityMP, err := msgpack.Marshal(newIdentityVal, identityBlock.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) + return resp, nil + } + + resp.NewIdentity = &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: newIdentityMP, + }, + } + } + // This is a signal to Terraform Core that we're doing the best we can to // shim the legacy type system of the SDK onto the Terraform type system // but we need it to cut us some slack. This setting should not be taken @@ -1940,3 +2169,22 @@ func configureDeferralAllowed(in *tfprotov5.ConfigureProviderClientCapabilities) return in.DeferralAllowed } + +// Resource Identity version of upgradeJSONState +func (s *GRPCProviderServer) upgradeJSONIdentity(ctx context.Context, version int64, m map[string]interface{}, res *Resource) (map[string]interface{}, error) { + var err error + + for _, upgrader := range res.Identity.IdentityUpgraders { + if version != upgrader.Version { + continue + } + + m, err = upgrader.Upgrade(ctx, m, s.provider.Meta()) + if err != nil { + return nil, err + } + version++ + } + + return m, nil +} diff --git a/helper/schema/grpc_provider_test.go b/helper/schema/grpc_provider_test.go index bdb22ceba08..274ebc986c9 100644 --- a/helper/schema/grpc_provider_test.go +++ b/helper/schema/grpc_provider_test.go @@ -3294,6 +3294,378 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { } } +func TestGRPCProviderServerGetResourceIdentitySchemas(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + Provider *Provider + Expected *tfprotov5.GetResourceIdentitySchemasResponse + }{ + "resources": { + Provider: &Provider{ + ResourcesMap: map[string]*Resource{ + "test_resource1": { + Identity: &ResourceIdentity{ + Version: 1, + Schema: map[string]*Schema{ + "test": { + Type: TypeString, + RequiredForImport: true, + OptionalForImport: false, + Description: "test resource", + }, + }, + }, + }, + "test_resource2": { + Identity: &ResourceIdentity{ + Schema: map[string]*Schema{ + "test2": { + Type: TypeString, + RequiredForImport: false, + OptionalForImport: true, + Description: "test resource 2", + }, + "test2-2": { + Type: TypeList, + RequiredForImport: false, + OptionalForImport: true, + Description: "test resource 2-2", + }, + "test2-3": { + Type: TypeInt, + RequiredForImport: false, + OptionalForImport: true, + Description: "test resource 2-3", + }, + }, + }, + }, + }, + }, + Expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource1": { + Version: 1, + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test", + Type: tftypes.String, + RequiredForImport: true, + OptionalForImport: false, + Description: "test resource", + }, + }, + }, + "test_resource2": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test2", + Type: tftypes.String, + RequiredForImport: false, + OptionalForImport: true, + Description: "test resource 2", + }, + { + Name: "test2-2", + Type: tftypes.List{ElementType: tftypes.String}, + RequiredForImport: false, + OptionalForImport: true, + Description: "test resource 2-2", + }, + { + Name: "test2-3", + Type: tftypes.Number, + RequiredForImport: false, + OptionalForImport: true, + Description: "test resource 2-3", + }, + }, + }, + }, + }, + }, + "primitive attributes": { + Provider: &Provider{ + ResourcesMap: map[string]*Resource{ + "test_resource": { + Identity: &ResourceIdentity{ + Schema: map[string]*Schema{ + "bool_attr": {Type: TypeBool, Description: "Boolean attribute"}, + "float_attr": {Type: TypeFloat, Description: "Float attribute"}, + "int_attr": {Type: TypeInt, Description: "Int attribute"}, + "list_bool_attr": {Type: TypeList, Elem: TypeBool, Description: "List Bool attribute"}, + "list_float_attr": {Type: TypeList, Elem: TypeFloat, Description: "List Float attribute"}, + "list_int_attr": {Type: TypeList, Elem: TypeInt, Description: "List Int attribute"}, + "list_str_attr": {Type: TypeList, Elem: TypeString, Description: "List String attribute"}, + "string_attr": {Type: TypeString, Description: "String attribute"}, + }, + }, + }, + }, + }, + Expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + {Name: "bool_attr", Type: tftypes.Bool, Description: "Boolean attribute"}, + {Name: "float_attr", Type: tftypes.Number, Description: "Float attribute"}, + {Name: "int_attr", Type: tftypes.Number, Description: "Int attribute"}, + {Name: "list_bool_attr", Type: tftypes.List{ElementType: tftypes.Bool}, Description: "List Bool attribute"}, + {Name: "list_float_attr", Type: tftypes.List{ElementType: tftypes.Number}, Description: "List Float attribute"}, + {Name: "list_int_attr", Type: tftypes.List{ElementType: tftypes.Number}, Description: "List Int attribute"}, + {Name: "list_str_attr", Type: tftypes.List{ElementType: tftypes.String}, Description: "List String attribute"}, + {Name: "string_attr", Type: tftypes.String, Description: "String attribute"}, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + + t.Run(name, func(t *testing.T) { + t.Parallel() + + server := NewGRPCProviderServer(testCase.Provider) + + testReq := &tfprotov5.GetResourceIdentitySchemasRequest{} + + resp, err := server.GetResourceIdentitySchemas(context.Background(), testReq) + + if err != nil { + t.Fatalf("unexpected gRPC error: %s", err) + } + + // Prevent false positives with random map access in testing + for _, schema := range resp.IdentitySchemas { + sort.Slice(schema.IdentityAttributes, func(i int, j int) bool { + return schema.IdentityAttributes[i].Name < schema.IdentityAttributes[j].Name + }) + } + + if diff := cmp.Diff(resp, testCase.Expected); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} + +// Based on TestUpgradeState_jsonState +func TestUpgradeResourceIdentity_jsonState(t *testing.T) { + r := &Resource{ + SchemaVersion: 1, + Identity: &ResourceIdentity{ + Version: 1, + Schema: map[string]*Schema{ + "id": { + Type: TypeString, + RequiredForImport: true, + OptionalForImport: false, + Description: "id of thing", + }, + }, + IdentityUpgraders: []IdentityUpgrader{ + { + Version: 0, + Type: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "identity": tftypes.String, + }, + }, + // upgrades former identity using "identity" as the attribute name to the new and shiny one just using "id" + Upgrade: func(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + id, ok := rawState["identity"].(string) + if !ok { + return nil, fmt.Errorf("identity not found in %#v", rawState) + } + rawState["id"] = id + delete(rawState, "identity") + return rawState, nil + }, + }, + }, + }, + } + + server := NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": r, + }, + }) + + req := &tfprotov5.UpgradeResourceIdentityRequest{ + TypeName: "test", + Version: 0, + RawIdentity: &tfprotov5.RawState{ + JSON: []byte(`{"identity":"Peter"}`), + }, + } + + resp, err := server.UpgradeResourceIdentity(context.Background(), req) + if err != nil { + t.Fatal(err) + } + + if len(resp.Diagnostics) > 0 { + for _, d := range resp.Diagnostics { + t.Errorf("%#v", d) + } + t.Fatal("error") + } + + val, err := msgpack.Unmarshal(resp.UpgradedIdentity.IdentityData.MsgPack, r.CoreIdentitySchema().ImpliedType()) + if err != nil { + t.Fatal(err) + } + + expected := cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("Peter"), + }) + + if !cmp.Equal(expected, val, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expected, val, valueComparer, equateEmpty)) + } +} + +// Based on TestUpgradeState_removedAttr +func TestUpgradeResourceIdentity_removedAttr(t *testing.T) { + r := &Resource{ + SchemaVersion: 1, + Identity: &ResourceIdentity{ + Version: 1, + Schema: map[string]*Schema{ + "id": { + Type: TypeString, + RequiredForImport: true, + OptionalForImport: false, + Description: "id of thing", + }, + }, + IdentityUpgraders: []IdentityUpgrader{ + { + Version: 0, + Type: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "identity": tftypes.String, + "removed": tftypes.String, + }, + }, + Upgrade: func(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + id, ok := rawState["identity"].(string) + if !ok { + return nil, fmt.Errorf("identity not found in %#v", rawState) + } + rawState["id"] = id + delete(rawState, "identity") + delete(rawState, "removed") + return rawState, nil + }, + }, + }, + }, + } + + server := NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": r, + }, + }) + + req := &tfprotov5.UpgradeResourceIdentityRequest{ + TypeName: "test", + Version: 0, + RawIdentity: &tfprotov5.RawState{ + JSON: []byte(`{"identity":"Peter", "removed":"to_be_removed"}`), + }, + } + + resp, err := server.UpgradeResourceIdentity(context.Background(), req) + if err != nil { + t.Fatal(err) + } + + if len(resp.Diagnostics) > 0 { + for _, d := range resp.Diagnostics { + t.Errorf("%#v", d) + } + t.Fatal("error") + } + + val, err := msgpack.Unmarshal(resp.UpgradedIdentity.IdentityData.MsgPack, r.CoreIdentitySchema().ImpliedType()) + if err != nil { + t.Fatal(err) + } + + expected := cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("Peter"), + }) + + if !cmp.Equal(expected, val, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expected, val, valueComparer, equateEmpty)) + } +} + +// Based on TestUpgradeState_jsonStateBigInt +// This test currently does not return the integer and does not recognize it as an attribute +func TestUpgradeResourceIdentity_jsonStateBigInt(t *testing.T) { + r := &Resource{ + UseJSONNumber: true, + SchemaVersion: 1, + Identity: &ResourceIdentity{ + Version: 1, + Schema: map[string]*Schema{ + "int": { + Type: TypeInt, + RequiredForImport: true, + OptionalForImport: false, + Description: "", + }, + }, + }, + } + + server := NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": r, + }, + }) + + req := &tfprotov5.UpgradeResourceIdentityRequest{ + TypeName: "test", + Version: 0, + RawIdentity: &tfprotov5.RawState{ + JSON: []byte(`{"int":7227701560655103598}`), + }, + } + + resp, err := server.UpgradeResourceIdentity(context.Background(), req) + if err != nil { + t.Fatal(err) + } + + if len(resp.Diagnostics) > 0 { + for _, d := range resp.Diagnostics { + t.Errorf("%#v", d) + } + t.Fatal("error") + } + + val, err := msgpack.Unmarshal(resp.UpgradedIdentity.IdentityData.MsgPack, r.CoreIdentitySchema().ImpliedType()) + if err != nil { + t.Fatal(err) + } + + expected := cty.ObjectVal(map[string]cty.Value{ + "int": cty.NumberIntVal(7227701560655103598), + }) + + if !cmp.Equal(expected, val, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expected, val, valueComparer, equateEmpty)) + } +} + func TestGRPCProviderServerGetMetadata(t *testing.T) { t.Parallel() @@ -4625,6 +4997,26 @@ func TestReadResource(t *testing.T) { Type: TypeString, Computed: true, }, + "test_list": { + Type: TypeList, + Elem: &Schema{ + Type: TypeString, + }, + Computed: true, + }, + }, + Identity: &ResourceIdentity{ + Version: 1, + Schema: map[string]*Schema{ + "instance_id": { + Type: TypeString, + RequiredForImport: true, + }, + "region": { + Type: TypeString, + OptionalForImport: true, + }, + }, }, ReadContext: func(ctx context.Context, d *ResourceData, meta interface{}) diag.Diagnostics { err := d.Set("test_bool", true) @@ -4637,6 +5029,15 @@ func TestReadResource(t *testing.T) { return diag.FromErr(err) } + identity, err := d.Identity() + if err != nil { + return diag.FromErr(err) + } + err = identity.Set("region", "new-region") + if err != nil { + return diag.FromErr(err) + } + return nil }, }, @@ -4644,17 +5045,36 @@ func TestReadResource(t *testing.T) { }), req: &tfprotov5.ReadResourceRequest{ TypeName: "test", + CurrentIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "instance_id": cty.String, + "region": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "instance_id": cty.StringVal("test-id"), + "region": cty.StringVal("test-region"), + }), + ), + }, + }, CurrentState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ "id": cty.String, "test_bool": cty.Bool, "test_string": cty.String, + "test_list": cty.List(cty.String), }), cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("test-id"), "test_bool": cty.BoolVal(false), "test_string": cty.StringVal("prior-state-val"), + "test_list": cty.ListVal([]cty.Value{ + cty.StringVal("hello"), + cty.StringVal("world"), + }), }), ), }, @@ -4666,14 +5086,33 @@ func TestReadResource(t *testing.T) { "id": cty.String, "test_bool": cty.Bool, "test_string": cty.String, + "test_list": cty.List(cty.String), }), cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("test-id"), "test_bool": cty.BoolVal(true), "test_string": cty.StringVal("new-state-val"), + "test_list": cty.ListVal([]cty.Value{ + cty.StringVal("hello"), + cty.StringVal("world"), + }), }), ), }, + NewIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "instance_id": cty.String, + "region": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "instance_id": cty.StringVal("test-id"), + "region": cty.StringVal("new-region"), + }), + ), + }, + }, }, }, "deferred-response-unknown-val": { @@ -4936,6 +5375,217 @@ func TestPlanResourceChange(t *testing.T) { UnsafeToUseLegacyTypeSystem: true, }, }, + "basic-plan-with-identity": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 4, + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + Optional: true, + }, + }, + Identity: &ResourceIdentity{ + Version: 1, + Schema: map[string]*Schema{ + "name": { + Type: TypeString, + RequiredForImport: true, + }, + }, + }, + }, + }, + }), + req: &tfprotov5.PlanResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "foo": cty.Number, + }), + cty.NullVal( + cty.Object(map[string]cty.Type{ + "foo": cty.Number, + }), + ), + ), + }, + ProposedNewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.Number, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.NullVal(cty.Number), + }), + ), + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.Number, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "foo": cty.NullVal(cty.Number), + }), + ), + }, + PriorIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "name": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("test-name"), + }), + ), + }, + }, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.Number, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.NullVal(cty.Number), + }), + ), + }, + RequiresReplace: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("id"), + }, + PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), + UnsafeToUseLegacyTypeSystem: true, + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "name": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("test-name"), + }), + ), + }, + }, + }, + }, + "new-resource-with-identity": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 4, + Schema: map[string]*Schema{ + "foo": { + Type: TypeString, + Optional: true, + }, + }, + Identity: &ResourceIdentity{ + Version: 1, + Schema: map[string]*Schema{ + "name": { + Type: TypeString, + RequiredForImport: true, + }, + }, + }, + CustomizeDiff: func(ctx context.Context, d *ResourceDiff, meta interface{}) error { + identity, err := d.Identity() + if err != nil { + return err + } + err = identity.Set("name", "Peter") + if err != nil { + return err + } + return nil + }, + }, + }, + }), + req: &tfprotov5.PlanResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.StringVal("baz"), + }), + ), + }, + ProposedNewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.StringVal("baz"), + }), + ), + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "foo": cty.StringVal("baz"), + }), + ), + }, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.StringVal("baz"), + }), + ), + }, + RequiresReplace: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("id"), + }, + PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), + UnsafeToUseLegacyTypeSystem: true, + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "name": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("Peter"), + }), + ), + }, + }, + }, + }, "basic-plan-EnableLegacyTypeSystemPlanErrors": { server: NewGRPCProviderServer(&Provider{ ResourcesMap: map[string]*Resource{ diff --git a/helper/schema/identity_data.go b/helper/schema/identity_data.go new file mode 100644 index 00000000000..9427d5a0be0 --- /dev/null +++ b/helper/schema/identity_data.go @@ -0,0 +1,138 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "log" + "reflect" + "strings" + "sync" +) + +type IdentityData struct { + // raw identity data will be stored internally + raw map[string]string + schema map[string]*Schema + + // Don't set + once sync.Once + writer *MapFieldWriter + reader *MapFieldReader + + panicOnError bool +} + +// Reading/writing data will be similar to the *schema.ResourceData flatmap +func (d *IdentityData) Get(key string) interface{} { + v, _ := d.GetOk(key) + return v +} + +func (d *IdentityData) GetOk(key string) (interface{}, bool) { + r := d.getRaw(key) + exists := r.Exists + if exists { + // If it exists, we also want to verify it is not the zero-value. + value := r.Value + zero := r.Schema.Type.Zero() + + if eq, ok := value.(Equal); ok { + exists = !eq.Equal(zero) + } else { + exists = !reflect.DeepEqual(value, zero) + } + } + + return r.Value, exists +} + +func (d *IdentityData) Set(key string, value interface{}) error { + d.once.Do(d.init) + + // If the value is a pointer to a non-struct, get its value and + // use that. This allows Set to take a pointer to primitives to + // simplify the interface. + reflectVal := reflect.ValueOf(value) + if reflectVal.Kind() == reflect.Ptr { + if reflectVal.IsNil() { + // If the pointer is nil, then the value is just nil + value = nil + } else { + // Otherwise, we dereference the pointer as long as its not + // a pointer to a struct, since struct pointers are allowed. + reflectVal = reflect.Indirect(reflectVal) + if reflectVal.Kind() != reflect.Struct { + value = reflectVal.Interface() + } + } + } + + err := d.writer.WriteField(strings.Split(key, "."), value) + if err != nil { + if d.panicOnError { + panic(err) + } else { + log.Printf("[ERROR] setting state: %s", err) + } + } + return err +} + +func (d *IdentityData) init() { + // re-use writers and readers to handle storing in flat map data structure + d.writer = &MapFieldWriter{Schema: d.schema} + for key, value := range d.raw { + err := d.writer.WriteField(strings.Split(key, "."), value) + if err != nil { + if d.panicOnError { + panic(err) + } else { + log.Printf("[ERROR] setting identity state: %s", err) + } + } + } + d.reader = &MapFieldReader{ + Schema: d.schema, + Map: BasicMapReader(d.writer.Map()), + } +} + +func (d *IdentityData) getRaw(key string) getResult { + var parts []string + if key != "" { + parts = strings.Split(key, ".") + } + + return d.get(parts) +} + +func (d *IdentityData) get(addr []string) getResult { + d.once.Do(d.init) + + result, err := d.reader.ReadField(addr) + + if err != nil { + panic(err) + } + + // If the result doesn't exist, then we set the value to the zero value + var schema *Schema + if schemaL := addrToSchema(addr, d.schema); len(schemaL) > 0 { + schema = schemaL[len(schemaL)-1] + } + + if result.Value == nil && schema != nil { + result.Value = result.ValueOrZero(schema) + } + + // Transform the FieldReadResult into a getResult. It might be worth + // merging these two structures one day. + return getResult{ + Value: result.Value, + ValueProcessed: result.ValueProcessed, + Computed: result.Computed, + Exists: result.Exists, + Schema: schema, + } +} diff --git a/helper/schema/resource.go b/helper/schema/resource.go index 32a21d2edfa..b10a51d3c34 100644 --- a/helper/schema/resource.go +++ b/helper/schema/resource.go @@ -89,6 +89,12 @@ type Resource struct { // their Versioning at any integer >= 1 SchemaVersion int + // Identity is a nested structure containing information about the structure + // and type of this resource's identity. This field is only valid when the + // Resource is a managed resource. + // This field, is optional. + Identity *ResourceIdentity + // MigrateState is responsible for updating an InstanceState with an old // version to the format expected by the current version of the Schema. // This field is only valid when the Resource is a managed resource. @@ -722,7 +728,11 @@ func (r *Resource) ShimInstanceStateFromValue(state cty.Value) (*terraform.Insta // We now rebuild the state through the ResourceData, so that the set indexes // match what helper/schema expects. - data, err := schemaMap(r.SchemaMap()).Data(s, nil) + var identity map[string]*Schema + if r.Identity != nil { + identity = r.Identity.Schema + } + data, err := schemaMapWithIdentity{r.SchemaMap(), identity}.Data(s, nil) if err != nil { return nil, err } @@ -895,7 +905,11 @@ func (r *Resource) Apply( s *terraform.InstanceState, d *terraform.InstanceDiff, meta interface{}) (*terraform.InstanceState, diag.Diagnostics) { - schema := schemaMap(r.SchemaMap()) + var identity map[string]*Schema + if r.Identity != nil { + identity = r.Identity.Schema + } + schema := schemaMapWithIdentity{r.SchemaMap(), identity} data, err := schema.Data(s, d) if err != nil { return s, diag.FromErr(err) @@ -1019,13 +1033,19 @@ func (r *Resource) SimpleDiff( c *terraform.ResourceConfig, meta interface{}) (*terraform.InstanceDiff, error) { - instanceDiff, err := schemaMap(r.SchemaMap()).Diff(ctx, s, c, r.CustomizeDiff, meta, false) + var identity map[string]*Schema + if r.Identity != nil { + identity = r.Identity.Schema + } + // TODO: figure out if it makes sense to be able to set identity in CustomizeDiff at all + instanceDiff, err := schemaMapWithIdentity{r.SchemaMap(), identity}.Diff(ctx, s, c, r.CustomizeDiff, meta, false) if err != nil { return instanceDiff, err } if instanceDiff == nil { instanceDiff = terraform.NewInstanceDiff() + instanceDiff.Identity = s.Identity // if we create a new diff, we need to copy the identity } // Make sure the old value is set in each of the instance diffs. @@ -1107,7 +1127,11 @@ func (r *Resource) RefreshWithoutUpgrade( } } - schema := schemaMap(r.SchemaMap()) + var identity map[string]*Schema + if r.Identity != nil { + identity = r.Identity.Schema + } + schema := schemaMapWithIdentity{r.SchemaMap(), identity} if r.Exists != nil { // Make a copy of data so that if it is modified it doesn't @@ -1417,7 +1441,8 @@ func (r *Resource) Data(s *terraform.InstanceState) *ResourceData { // TODO: May be able to be removed with the above ResourceData function. func (r *Resource) TestResourceData() *ResourceData { return &ResourceData{ - schema: r.SchemaMap(), + schema: r.SchemaMap(), + identitySchema: r.Identity.Schema, } } diff --git a/helper/schema/resource_data.go b/helper/schema/resource_data.go index 5129c925c4c..50090f3706e 100644 --- a/helper/schema/resource_data.go +++ b/helper/schema/resource_data.go @@ -28,18 +28,20 @@ import ( // The most relevant methods to take a look at are Get and Set. type ResourceData struct { // Settable (internally) - schema map[string]*Schema - config *terraform.ResourceConfig - state *terraform.InstanceState - diff *terraform.InstanceDiff - meta map[string]interface{} - timeouts *ResourceTimeout - providerMeta cty.Value + schema map[string]*Schema + identitySchema map[string]*Schema + config *terraform.ResourceConfig + state *terraform.InstanceState + diff *terraform.InstanceDiff + meta map[string]interface{} + timeouts *ResourceTimeout + providerMeta cty.Value // Don't set multiReader *MultiLevelFieldReader setWriter *MapFieldWriter newState *terraform.InstanceState + newIdentity *IdentityData partial bool once sync.Once isNew bool @@ -409,6 +411,36 @@ func (d *ResourceData) State() *terraform.InstanceState { result.Tainted = d.state.Tainted } + // If the ResourceData has an identitySchema: + // copy over identity data (by getting it so we also include changes) + // In order to build the final state attributes, we read the full + // attribute set as a map[string]interface{}, write it to a MapFieldWriter, + // and then use that map. + if d.identitySchema != nil { + rawMapIdentity := make(map[string]interface{}) + identityData, err := d.Identity() + // This error shouldn't happen, as we check for the identity schema first + if err == nil { + for k := range d.identitySchema { + raw := identityData.get([]string{k}) + if raw.Exists { + rawMapIdentity[k] = raw.Value + if raw.ValueProcessed != nil { + rawMapIdentity[k] = raw.ValueProcessed + } + } + } + + mapWIdentity := &MapFieldWriter{Schema: d.identitySchema} + if err := mapWIdentity.WriteField(nil, rawMapIdentity); err != nil { + log.Printf("[ERR] Error writing identity fields: %s", err) + return nil + } + + result.Identity = mapWIdentity.Map() + } + } + return &result } @@ -701,3 +733,29 @@ func (d *ResourceData) GetRawPlan() cty.Value { } return cty.NullVal(schemaMap(d.schema).CoreConfigSchema().ImpliedType()) } + +// IdentityData is only available for managed resources, data sources +// will return an error. // TODO: return error in case of data sources +func (d *ResourceData) Identity() (*IdentityData, error) { + // return memoized value if available + if d.newIdentity != nil { + return d.newIdentity, nil + } + + if d.identitySchema == nil { + return nil, fmt.Errorf("Resource does not have Identity schema. Please set one in order to use Identity(). This is always a problem in the provider code.") + } + + var identityData map[string]string + if d.state != nil && d.state.Identity != nil { + identityData = d.state.Identity + } + + d.newIdentity = &IdentityData{ + schema: d.identitySchema, + raw: identityData, + panicOnError: d.panicOnError, + } + + return d.newIdentity, nil +} diff --git a/helper/schema/resource_diff.go b/helper/schema/resource_diff.go index 9f7dab683b4..300c16de4b9 100644 --- a/helper/schema/resource_diff.go +++ b/helper/schema/resource_diff.go @@ -118,6 +118,9 @@ type ResourceDiff struct { // The schema for the resource being worked on. schema map[string]*Schema + // The identity schema for the resource being worked on. + identitySchema map[string]*Schema + // The current config for this resource. config *terraform.ResourceConfig @@ -145,15 +148,18 @@ type ResourceDiff struct { // Tracks which keys were flagged as forceNew. These keys are not saved in // newWriter, but we need to track them so that they can be re-diffed later. forcedNewKeys map[string]bool + + newIdentity *IdentityData } // newResourceDiff creates a new ResourceDiff instance. -func newResourceDiff(schema map[string]*Schema, config *terraform.ResourceConfig, state *terraform.InstanceState, diff *terraform.InstanceDiff) *ResourceDiff { +func newResourceDiff(schema schemaMapWithIdentity, config *terraform.ResourceConfig, state *terraform.InstanceState, diff *terraform.InstanceDiff) *ResourceDiff { d := &ResourceDiff{ - config: config, - state: state, - diff: diff, - schema: schema, + config: config, + state: state, + diff: diff, + schema: schema.schemaMap, + identitySchema: schema.identitySchema, } d.newWriter = &newValueWriter{ @@ -682,3 +688,22 @@ func (d *ResourceDiff) checkKey(key, caller string, nested bool) error { } return nil } + +func (d *ResourceDiff) Identity() (*IdentityData, error) { + // return memoized value if available + if d.newIdentity != nil { + return d.newIdentity, nil + } + + identity := map[string]string{} + if d.state != nil && d.state.Identity != nil { + identity = d.state.Identity + } + + d.newIdentity = &IdentityData{ + schema: d.identitySchema, + raw: identity, + } + + return d.newIdentity, nil +} diff --git a/helper/schema/resource_diff_test.go b/helper/schema/resource_diff_test.go index ef5198214bf..bd0cfd4ca0d 100644 --- a/helper/schema/resource_diff_test.go +++ b/helper/schema/resource_diff_test.go @@ -31,17 +31,18 @@ func testSetFunc(v interface{}) int { // resourceDiffTestCase provides a test case struct for SetNew and SetDiff. type resourceDiffTestCase struct { - Name string - Schema map[string]*Schema - State *terraform.InstanceState - Config *terraform.ResourceConfig - Diff *terraform.InstanceDiff - Key string - OldValue interface{} - NewValue interface{} - Expected *terraform.InstanceDiff - ExpectedKeys []string - ExpectedError bool + Name string + Schema map[string]*Schema + IdentitySchema map[string]*Schema + State *terraform.InstanceState + Config *terraform.ResourceConfig + Diff *terraform.InstanceDiff + Key string + OldValue interface{} + NewValue interface{} + Expected *terraform.InstanceDiff + ExpectedKeys []string + ExpectedError bool } // testDiffCases produces a list of test cases for use with SetNew and SetDiff. @@ -645,7 +646,7 @@ func TestSetNew(t *testing.T) { for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { m := schemaMap(tc.Schema) - d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff) + d := newResourceDiff(schemaMapWithIdentity{tc.Schema, tc.IdentitySchema}, tc.Config, tc.State, tc.Diff) err := d.SetNew(tc.Key, tc.NewValue) switch { case err != nil && !tc.ExpectedError: @@ -672,7 +673,7 @@ func TestSetNewComputed(t *testing.T) { for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { m := schemaMap(tc.Schema) - d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff) + d := newResourceDiff(schemaMapWithIdentity{tc.Schema, tc.IdentitySchema}, tc.Config, tc.State, tc.Diff) err := d.SetNewComputed(tc.Key) switch { case err != nil && !tc.ExpectedError: @@ -940,7 +941,7 @@ func TestForceNew(t *testing.T) { } for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { - m := schemaMap(tc.Schema) + m := schemaMapWithIdentity{tc.Schema, tc.IdentitySchema} d := newResourceDiff(m, tc.Config, tc.State, tc.Diff) err := d.ForceNew(tc.Key) switch { @@ -952,7 +953,7 @@ func TestForceNew(t *testing.T) { return } for _, k := range d.UpdatedKeys() { - if err := m.diff(context.Background(), k, m[k], tc.Diff, d, false); err != nil { + if err := m.diff(context.Background(), k, m.schemaMap[k], tc.Diff, d, false); err != nil { t.Fatalf("bad: %s", err) } } @@ -1189,7 +1190,7 @@ func TestClear(t *testing.T) { } for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { - m := schemaMap(tc.Schema) + m := schemaMapWithIdentity{tc.Schema, tc.IdentitySchema} d := newResourceDiff(m, tc.Config, tc.State, tc.Diff) err := d.Clear(tc.Key) switch { @@ -1201,7 +1202,7 @@ func TestClear(t *testing.T) { return } for _, k := range d.UpdatedKeys() { - if err := m.diff(context.Background(), k, m[k], tc.Diff, d, false); err != nil { + if err := m.diff(context.Background(), k, m.schemaMap[k], tc.Diff, d, false); err != nil { t.Fatalf("bad: %s", err) } } @@ -1438,12 +1439,12 @@ func TestGetChangedKeysPrefix(t *testing.T) { } for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { - m := schemaMap(tc.Schema) + m := schemaMapWithIdentity{tc.Schema, tc.IdentitySchema} d := newResourceDiff(m, tc.Config, tc.State, tc.Diff) keys := d.GetChangedKeysPrefix(tc.Key) for _, k := range d.UpdatedKeys() { - if err := m.diff(context.Background(), k, m[k], tc.Diff, d, false); err != nil { + if err := m.diff(context.Background(), k, m.schemaMap[k], tc.Diff, d, false); err != nil { t.Fatalf("bad: %s", err) } } @@ -1459,14 +1460,15 @@ func TestGetChangedKeysPrefix(t *testing.T) { func TestResourceDiffGetOkExists(t *testing.T) { cases := []struct { - Name string - Schema map[string]*Schema - State *terraform.InstanceState - Config *terraform.ResourceConfig - Diff *terraform.InstanceDiff - Key string - Value interface{} - Ok bool + Name string + Schema map[string]*Schema + IdentitySchema map[string]*Schema + State *terraform.InstanceState + Config *terraform.ResourceConfig + Diff *terraform.InstanceDiff + Key string + Value interface{} + Ok bool }{ /* * Primitives @@ -1814,7 +1816,7 @@ func TestResourceDiffGetOkExists(t *testing.T) { for i, tc := range cases { t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { - d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff) + d := newResourceDiff(schemaMapWithIdentity{tc.Schema, tc.IdentitySchema}, tc.Config, tc.State, tc.Diff) v, ok := d.GetOkExists(tc.Key) if s, ok := v.(*Set); ok { @@ -1833,12 +1835,13 @@ func TestResourceDiffGetOkExists(t *testing.T) { func TestResourceDiffGetOkExistsSetNew(t *testing.T) { tc := struct { - Schema map[string]*Schema - State *terraform.InstanceState - Diff *terraform.InstanceDiff - Key string - Value interface{} - Ok bool + Schema map[string]*Schema + IdentitySchema map[string]*Schema + State *terraform.InstanceState + Diff *terraform.InstanceDiff + Key string + Value interface{} + Ok bool }{ Schema: map[string]*Schema{ "availability_zone": { @@ -1859,7 +1862,7 @@ func TestResourceDiffGetOkExistsSetNew(t *testing.T) { Ok: true, } - d := newResourceDiff(tc.Schema, testConfig(t, map[string]interface{}{}), tc.State, tc.Diff) + d := newResourceDiff(schemaMapWithIdentity{tc.Schema, tc.IdentitySchema}, testConfig(t, map[string]interface{}{}), tc.State, tc.Diff) if err := d.SetNew(tc.Key, tc.Value); err != nil { t.Fatalf("unexpected SetNew error: %s", err) @@ -1880,12 +1883,13 @@ func TestResourceDiffGetOkExistsSetNew(t *testing.T) { func TestResourceDiffGetOkExistsSetNewComputed(t *testing.T) { tc := struct { - Schema map[string]*Schema - State *terraform.InstanceState - Diff *terraform.InstanceDiff - Key string - Value interface{} - Ok bool + Schema map[string]*Schema + IdentitySchema map[string]*Schema + State *terraform.InstanceState + Diff *terraform.InstanceDiff + Key string + Value interface{} + Ok bool }{ Schema: map[string]*Schema{ "availability_zone": { @@ -1910,7 +1914,7 @@ func TestResourceDiffGetOkExistsSetNewComputed(t *testing.T) { Ok: false, } - d := newResourceDiff(tc.Schema, testConfig(t, map[string]interface{}{}), tc.State, tc.Diff) + d := newResourceDiff(schemaMapWithIdentity{tc.Schema, tc.IdentitySchema}, testConfig(t, map[string]interface{}{}), tc.State, tc.Diff) if err := d.SetNewComputed(tc.Key); err != nil { t.Fatalf("unexpected SetNewComputed error: %s", err) @@ -1925,13 +1929,14 @@ func TestResourceDiffGetOkExistsSetNewComputed(t *testing.T) { func TestResourceDiffNewValueKnown(t *testing.T) { cases := []struct { - Name string - Schema map[string]*Schema - State *terraform.InstanceState - Config *terraform.ResourceConfig - Diff *terraform.InstanceDiff - Key string - Expected bool + Name string + Schema map[string]*Schema + IdentitySchema map[string]*Schema + State *terraform.InstanceState + Config *terraform.ResourceConfig + Diff *terraform.InstanceDiff + Key string + Expected bool }{ { Name: "in config, no state", @@ -2102,7 +2107,7 @@ func TestResourceDiffNewValueKnown(t *testing.T) { for i, tc := range cases { t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { - d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff) + d := newResourceDiff(schemaMapWithIdentity{tc.Schema, tc.IdentitySchema}, tc.Config, tc.State, tc.Diff) actual := d.NewValueKnown(tc.Key) if tc.Expected != actual { @@ -2114,13 +2119,14 @@ func TestResourceDiffNewValueKnown(t *testing.T) { func TestResourceDiffNewValueKnownSetNew(t *testing.T) { tc := struct { - Schema map[string]*Schema - State *terraform.InstanceState - Config *terraform.ResourceConfig - Diff *terraform.InstanceDiff - Key string - Value interface{} - Expected bool + Schema map[string]*Schema + IdentitySchema map[string]*Schema + State *terraform.InstanceState + Config *terraform.ResourceConfig + Diff *terraform.InstanceDiff + Key string + Value interface{} + Expected bool }{ Schema: map[string]*Schema{ "availability_zone": { @@ -2154,7 +2160,7 @@ func TestResourceDiffNewValueKnownSetNew(t *testing.T) { Expected: true, } - d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff) + d := newResourceDiff(schemaMapWithIdentity{tc.Schema, tc.IdentitySchema}, tc.Config, tc.State, tc.Diff) if err := d.SetNew(tc.Key, tc.Value); err != nil { t.Fatalf("unexpected SetNew error: %s", err) @@ -2168,12 +2174,13 @@ func TestResourceDiffNewValueKnownSetNew(t *testing.T) { func TestResourceDiffNewValueKnownSetNewComputed(t *testing.T) { tc := struct { - Schema map[string]*Schema - State *terraform.InstanceState - Config *terraform.ResourceConfig - Diff *terraform.InstanceDiff - Key string - Expected bool + Schema map[string]*Schema + IdentitySchema map[string]*Schema + State *terraform.InstanceState + Config *terraform.ResourceConfig + Diff *terraform.InstanceDiff + Key string + Expected bool }{ Schema: map[string]*Schema{ "availability_zone": { @@ -2194,7 +2201,7 @@ func TestResourceDiffNewValueKnownSetNewComputed(t *testing.T) { Expected: false, } - d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff) + d := newResourceDiff(schemaMapWithIdentity{tc.Schema, tc.IdentitySchema}, tc.Config, tc.State, tc.Diff) if err := d.SetNewComputed(tc.Key); err != nil { t.Fatalf("unexpected SetNewComputed error: %s", err) @@ -2208,11 +2215,12 @@ func TestResourceDiffNewValueKnownSetNewComputed(t *testing.T) { func TestResourceDiffHasChanges(t *testing.T) { cases := []struct { - Schema map[string]*Schema - State *terraform.InstanceState - Diff *terraform.InstanceDiff - Keys []string - Change bool + Schema map[string]*Schema + IdentitySchema map[string]*Schema + State *terraform.InstanceState + Diff *terraform.InstanceDiff + Keys []string + Change bool }{ // empty call d.HasChanges() { @@ -2301,7 +2309,7 @@ func TestResourceDiffHasChanges(t *testing.T) { } for i, tc := range cases { - d := newResourceDiff(tc.Schema, testConfig(t, map[string]interface{}{}), tc.State, tc.Diff) + d := newResourceDiff(schemaMapWithIdentity{tc.Schema, tc.IdentitySchema}, testConfig(t, map[string]interface{}{}), tc.State, tc.Diff) actual := d.HasChanges(tc.Keys...) if actual != tc.Change { diff --git a/helper/schema/resource_identity.go b/helper/schema/resource_identity.go new file mode 100644 index 00000000000..a5f24c191ee --- /dev/null +++ b/helper/schema/resource_identity.go @@ -0,0 +1,72 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Implementation of a single identity schema version upgrade. +type IdentityUpgrader struct { + // Version is the version schema that this Upgrader will handle, converting + // it to Version+1. + Version int64 + + // Type describes the schema that this function can upgrade. Type is + // required to decode the schema if the state was stored in a legacy + // flatmap format. + Type tftypes.Type + + // Upgrade takes the JSON encoded state and the provider meta value, and + // upgrades the state one single schema version. The provided state is + // decoded into the default json types using a map[string]interface{}. It + // is up to the StateUpgradeFunc to ensure that the returned value can be + // encoded using the new schema. + Upgrade ResourceIdentityUpgradeFunc +} + +type ResourceIdentity struct { + // Version is the identity schema version. + Version int64 + + // Schema is the structure and type information for the identity. + // The types allowed in this Schema will be more restricted than + // previous resource schemas. + Schema map[string]*Schema + + // New struct, will be similar to (Resource).StateUpgraders + IdentityUpgraders []IdentityUpgrader +} + +// Function signature for an identity schema version upgrade handler. +// +// The Context parameter stores SDK information, such as loggers. It also +// is wired to receive any cancellation from Terraform such as a system or +// practitioner sending SIGINT (Ctrl-c). +// +// The map[string]interface{} parameter contains the previous identity schema +// version data for a managed resource instance. The keys are top level attribute +// names mapped to values that can be type asserted similar to +// fetching values using the ResourceData Get* methods: +// +// - TypeBool: bool +// - TypeFloat: float +// - TypeInt: int +// - TypeList: []interface{} +// - TypeString: string +// +// In certain scenarios, the map may be nil, so checking for that condition +// upfront is recommended to prevent potential panics. +// +// The interface{} parameter is the result of the Provider type +// ConfigureFunc field execution. If the Provider does not define +// a ConfigureFunc, this will be nil. This parameter is conventionally +// used to store API clients and other provider instance specific data. +// +// The map[string]interface{} return parameter should contain the upgraded +// identity schema version data for a managed resource instance. Values must +// align to the typing mentioned above. +type ResourceIdentityUpgradeFunc func(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) diff --git a/helper/schema/schema.go b/helper/schema/schema.go index ea6cd768d18..1d56ec37797 100644 --- a/helper/schema/schema.go +++ b/helper/schema/schema.go @@ -93,6 +93,22 @@ type Schema struct { // with Required. Optional bool + // RequiredForImport indicates whether the practitioner must enter a value + // in the import block for this attribute when importing a resource. + // + // RequiredForImport is only valid for identity schemas and either + // RequiredForImport or OptionalForImport must be set to true. + RequiredForImport bool + + // OptionalForImport indicates whether the practitioner can choose to not + // enter a value in the import block for this attribute when importing a + // resource. For example, this can be data that would normally be the default + // of the configured provider running the import. + // + // OptionalForImport is only valid for identity schemas and either + // RequiredForImport or OptionalForImport must be set to true. + OptionalForImport bool + // Computed indicates whether the provider may return its own value for // this attribute or not. Computed cannot be used with Required. If // Required and Optional are both false, the attribute will be considered @@ -631,6 +647,13 @@ type InternalMap = schemaMap // schemaMap is a wrapper that adds nice functions on top of schemas. type schemaMap map[string]*Schema +// schemaMapWithIdentity is a wrapper around schemaMap that allows passing an +// identity schema to ResourceData{} structs that are returned. +type schemaMapWithIdentity struct { + schemaMap + identitySchema map[string]*Schema +} + func (m schemaMap) panicOnError() bool { return os.Getenv("TF_ACC") != "" } @@ -638,17 +661,27 @@ func (m schemaMap) panicOnError() bool { // Data returns a ResourceData for the given schema, state, and diff. // // The diff is optional. -func (m schemaMap) Data( +func (m schemaMapWithIdentity) Data( s *terraform.InstanceState, d *terraform.InstanceDiff) (*ResourceData, error) { return &ResourceData{ - schema: m, - state: s, - diff: d, - panicOnError: m.panicOnError(), + schema: m.schemaMap, + identitySchema: m.identitySchema, + state: s, + diff: d, + panicOnError: m.panicOnError(), }, nil } +// Data returns a ResourceData for the given schema, state, and diff. +// +// The diff is optional. +func (m schemaMap) Data( + s *terraform.InstanceState, + d *terraform.InstanceDiff) (*ResourceData, error) { + return schemaMapWithIdentity{m, nil}.Data(s, d) +} + // DeepCopy returns a copy of this schemaMap. The copy can be safely modified // without affecting the original. func (m *schemaMap) DeepCopy() schemaMap { @@ -659,9 +692,20 @@ func (m *schemaMap) DeepCopy() schemaMap { return *copiedMap.(*schemaMap) } +// DeepCopy returns a copy of this schemaMapWithIdentity. The copy can be safely modified +// without affecting the original. +func (m *schemaMapWithIdentity) DeepCopy() schemaMapWithIdentity { + copiedMap := schemaMapWithIdentity{} + copiedMap.schemaMap = m.schemaMap.DeepCopy() + identitySchema := schemaMap(m.identitySchema) + copiedMap.identitySchema = identitySchema.DeepCopy() + + return copiedMap +} + // Diff returns the diff for a resource given the schema map, // state, and configuration. -func (m schemaMap) Diff( +func (m schemaMapWithIdentity) Diff( ctx context.Context, s *terraform.InstanceState, c *terraform.ResourceConfig, @@ -677,16 +721,18 @@ func (m schemaMap) Diff( result.RawConfig = s.RawConfig result.RawState = s.RawState result.RawPlan = s.RawPlan + result.Identity = s.Identity } d := &ResourceData{ - schema: m, - state: s, - config: c, - panicOnError: m.panicOnError(), + schema: m.schemaMap, + identitySchema: m.identitySchema, + state: s, + config: c, + panicOnError: m.panicOnError(), } - for k, schema := range m { + for k, schema := range m.schemaMap { err := m.diff(ctx, k, schema, result, d, false) if err != nil { return nil, err @@ -714,11 +760,36 @@ func (m schemaMap) Diff( return nil, err } for _, k := range rd.UpdatedKeys() { - err := m.diff(ctx, k, mc[k], result, rd, false) + err := m.diff(ctx, k, mc.schemaMap[k], result, rd, false) if err != nil { return nil, err } } + // copy over identity data (by getting it so we also include changes) + // In order to build the final identity attributes, we read the full + // attribute set as a map[string]interface{}, write it to a MapFieldWriter, + // and then use that map. + rawMapIdentity := make(map[string]interface{}) + identityData, err := rd.Identity() + if err == nil && d.identitySchema != nil { + for k := range d.identitySchema { + raw := identityData.get([]string{k}) + if raw.Exists && !raw.Computed { + rawMapIdentity[k] = raw.Value + if raw.ValueProcessed != nil { + rawMapIdentity[k] = raw.ValueProcessed + } + } + } + + mapWIdentity := &MapFieldWriter{Schema: d.identitySchema} + if err := mapWIdentity.WriteField(nil, rawMapIdentity); err != nil { + log.Printf("[ERR] Error writing identity fields: %s", err) + return nil, err + } + + result.Identity = mapWIdentity.Map() + } // TODO: else log error? } if handleRequiresNew { @@ -743,7 +814,7 @@ func (m schemaMap) Diff( d.init() // Perform the diff again - for k, schema := range m { + for k, schema := range m.schemaMap { err := m.diff(ctx, k, schema, result2, d, false) if err != nil { return nil, err @@ -758,11 +829,37 @@ func (m schemaMap) Diff( return nil, err } for _, k := range rd.UpdatedKeys() { - err := m.diff(ctx, k, mc[k], result2, rd, false) + err := m.diff(ctx, k, mc.schemaMap[k], result2, rd, false) if err != nil { return nil, err } } + // copy over identity data (by getting it so we also include changes) + // In order to build the final identity attributes, we read the full + // attribute set as a map[string]interface{}, write it to a MapFieldWriter, + // and then use that map. + rawMapIdentity := make(map[string]interface{}) + identityData, err := rd.Identity() + if err == nil && d.identitySchema != nil { + for k := range d.identitySchema { + raw := identityData.get([]string{k}) + if raw.Exists && !raw.Computed { + rawMapIdentity[k] = raw.Value + if raw.ValueProcessed != nil { + rawMapIdentity[k] = raw.ValueProcessed + } + } + } + + mapWIdentity := &MapFieldWriter{Schema: d.identitySchema} + if err := mapWIdentity.WriteField(nil, rawMapIdentity); err != nil { + log.Printf("[ERR] Error writing identity fields: %s", err) + return nil, err + } + + result2.Identity = mapWIdentity.Map() + } // TODO: else log error? + } // Force all the fields to not force a new since we know what we @@ -817,6 +914,18 @@ func (m schemaMap) Diff( return result, nil } +// Diff returns the diff for a resource given the schema map, +// state, and configuration. +func (m schemaMap) Diff( + ctx context.Context, + s *terraform.InstanceState, + c *terraform.ResourceConfig, + customizeDiff CustomizeDiffFunc, + meta interface{}, + handleRequiresNew bool) (*terraform.InstanceDiff, error) { + return schemaMapWithIdentity{m, nil}.Diff(ctx, s, c, customizeDiff, meta, handleRequiresNew) +} + // Validate validates the configuration against this schema mapping. func (m schemaMap) Validate(c *terraform.ResourceConfig) diag.Diagnostics { return m.validateObject("", m, c, cty.Path{}) @@ -829,6 +938,7 @@ func (m schemaMap) InternalValidate(topSchemaMap schemaMap) error { return m.internalValidate(topSchemaMap, false) } +// TODO: Think about how to check something is a resource Identity so that we can check if RequiredForImport or OptionalForImport is set func (m schemaMap) internalValidate(topSchemaMap schemaMap, attrsOnly bool) error { if topSchemaMap == nil { topSchemaMap = m @@ -902,6 +1012,10 @@ func (m schemaMap) internalValidate(topSchemaMap schemaMap, attrsOnly bool) erro return fmt.Errorf("%s: Default cannot be set with WriteOnly", k) } + if v.WriteOnly && v.Default != nil { + return fmt.Errorf("%s: Default cannot be set with WriteOnly", k) + } + if v.WriteOnly && v.DefaultFunc != nil { return fmt.Errorf("%s: DefaultFunc cannot be set with WriteOnly", k) } @@ -1652,7 +1766,7 @@ func (m schemaMap) diffString( // DiffSuppressOnRefresh, checks whether the new value is materially different // than the old and if not it overwrites the new value with the old one, // in-place. -func (m schemaMap) handleDiffSuppressOnRefresh(ctx context.Context, oldState, newState *terraform.InstanceState) { +func (m schemaMapWithIdentity) handleDiffSuppressOnRefresh(ctx context.Context, oldState, newState *terraform.InstanceState) { if newState == nil || oldState == nil { return // nothing to do, then } @@ -1672,7 +1786,7 @@ func (m schemaMap) handleDiffSuppressOnRefresh(ctx context.Context, oldState, ne continue // no change to test } - schemaList := addrToSchema(strings.Split(k, "."), m) + schemaList := addrToSchema(strings.Split(k, "."), m.schemaMap) if len(schemaList) == 0 { continue // no schema? weird, but not our responsibility to handle } diff --git a/helper/schema/schema_test.go b/helper/schema/schema_test.go index 988e43ea027..866e22ce816 100644 --- a/helper/schema/schema_test.go +++ b/helper/schema/schema_test.go @@ -3114,8 +3114,8 @@ func TestSchemaMap_Diff(t *testing.T) { t.Fatalf("err: %s", err) } - if !reflect.DeepEqual(tc.Diff, d) { - t.Fatalf("expected:\n%#v\n\ngot:\n%#v", tc.Diff, d) + if diff := cmp.Diff(tc.Diff, d); diff != "" { + t.Fatalf("mismatch (-want +got):\n%s", diff) } }) } @@ -5878,7 +5878,7 @@ func TestSchema_DiffSuppressOnRefresh(t *testing.T) { for tn, tc := range cases { t.Run(tn, func(t *testing.T) { - schema := tc.Schema + schema := schemaMapWithIdentity{tc.Schema, nil} // TODO: add IdentitySchema here priorState := &terraform.InstanceState{ Attributes: tc.PriorState, } diff --git a/helper/schema/shims.go b/helper/schema/shims.go index e8baebd70cf..9b8159fd297 100644 --- a/helper/schema/shims.go +++ b/helper/schema/shims.go @@ -43,7 +43,11 @@ func diffFromValues(ctx context.Context, prior, planned, config cty.Value, res * removeConfigUnknowns(cfg.Config) removeConfigUnknowns(cfg.Raw) - diff, err := schemaMap(res.SchemaMap()).Diff(ctx, instanceState, cfg, cust, nil, false) + var identity map[string]*Schema + if res.Identity != nil { + identity = res.Identity.Schema + } + diff, err := schemaMapWithIdentity{res.SchemaMap(), identity}.Diff(ctx, instanceState, cfg, cust, nil, false) if err != nil { return nil, err } diff --git a/internal/configs/configschema/schema.go b/internal/configs/configschema/schema.go index 983d20bdf08..5410376cfad 100644 --- a/internal/configs/configschema/schema.go +++ b/internal/configs/configschema/schema.go @@ -95,6 +95,16 @@ type Attribute struct { // Practitioners that choose a value for this attribute with older // versions of Terraform will receive an error. WriteOnly bool + + // RequiredForImport, if set to true, specifies that an omitted or null value is + // not permitted when importing by the identity. This field conflicts with OptionalForImport. + // Only valid for identity schemas. + RequiredForImport bool + + // OptionalForImport, if set to true, specifies that an omitted or null value is + // permitted when importing by the identity. This field conflicts with RequiredForImport. + // Only valid for identity schemas. + OptionalForImport bool } // NestedBlock represents the embedding of one block within another. diff --git a/internal/plugin/convert/schema.go b/internal/plugin/convert/schema.go index a02aaec0078..d6fd7d8d9ab 100644 --- a/internal/plugin/convert/schema.go +++ b/internal/plugin/convert/schema.go @@ -172,6 +172,30 @@ func ConfigSchemaToProto(ctx context.Context, b *configschema.Block) *tfprotov5. return block } +func ConfigIdentitySchemaToProto(ctx context.Context, identitySchema *configschema.Block) []*tfprotov5.ResourceIdentitySchemaAttribute { + output := make([]*tfprotov5.ResourceIdentitySchemaAttribute, 0) + + for name, a := range identitySchema.Attributes { + + attr := &tfprotov5.ResourceIdentitySchemaAttribute{ + Name: name, + Description: a.Description, + OptionalForImport: a.OptionalForImport, + RequiredForImport: a.RequiredForImport, + } + + var err error + attr.Type, err = tftypeFromCtyType(a.Type) + if err != nil { + panic(err) + } + + output = append(output, attr) + } + + return output +} + func protoStringKind(ctx context.Context, k configschema.StringKind) tfprotov5.StringKind { switch k { default: diff --git a/terraform/diff.go b/terraform/diff.go index 3b4179b4b3b..383da828f97 100644 --- a/terraform/diff.go +++ b/terraform/diff.go @@ -51,6 +51,10 @@ type InstanceDiff struct { // meant to be used for additional data a resource may want to pass through. // The value here must only contain Go primitives and collections. Meta map[string]interface{} + + // Identity is the identity data used to track resource identity + // starting in Terraform 1.12+ + Identity map[string]string } func (d *InstanceDiff) Lock() { d.mu.Lock() } @@ -663,7 +667,8 @@ func (d *InstanceDiff) Empty() bool { return !d.Destroy && !d.DestroyTainted && !d.DestroyDeposed && - len(d.Attributes) == 0 + len(d.Attributes) == 0 && + len(d.Identity) == 0 } // Equal compares two diffs for exact equality. diff --git a/terraform/state.go b/terraform/state.go index 60723de772a..2004b65dc61 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -1305,6 +1305,10 @@ type InstanceState struct { // and collections. Meta map[string]interface{} `json:"meta"` + // Identity is the identity data used to track resource identity + // starting in Terraform 1.12+ + Identity map[string]string `json:"identity"` + ProviderMeta cty.Value RawConfig cty.Value @@ -1330,6 +1334,9 @@ func (s *InstanceState) init() { if s.Meta == nil { s.Meta = make(map[string]interface{}) } + if s.Identity == nil { + s.Identity = make(map[string]string) + } s.Ephemeral.init() } @@ -1391,6 +1398,7 @@ func (s *InstanceState) Set(from *InstanceState) { s.Ephemeral = from.Ephemeral s.Meta = from.Meta s.Tainted = from.Tainted + s.Identity = from.Identity } func (s *InstanceState) DeepCopy() *InstanceState { @@ -1470,6 +1478,8 @@ func (s *InstanceState) Equal(other *InstanceState) bool { return false } + // TODO: compare identity + return true } @@ -1547,6 +1557,8 @@ func (s *InstanceState) String() string { buf.WriteString(fmt.Sprintf("%s = %s\n", ak, av)) } + // TODO: add identity + buf.WriteString(fmt.Sprintf("Tainted = %t\n", s.Tainted)) return buf.String() From 9eb7da0be3e9a35771af84fae0a37a53d96fb833 Mon Sep 17 00:00:00 2001 From: Ansgar Mertens Date: Wed, 19 Mar 2025 15:51:23 +0100 Subject: [PATCH 14/53] update terraform-plugin-go to v0.27.0-alpha.1 and add changie entry (#1449) * update terraform-plugin-go to v0.27.0-alpha.1 * add changie entry for resource identity alpha feature * Fix typo in NOTES-20250319-150717.yaml --- .changes/unreleased/NOTES-20250319-150717.yaml | 8 ++++++++ go.mod | 2 +- go.sum | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 .changes/unreleased/NOTES-20250319-150717.yaml diff --git a/.changes/unreleased/NOTES-20250319-150717.yaml b/.changes/unreleased/NOTES-20250319-150717.yaml new file mode 100644 index 00000000000..c4807b7d8a6 --- /dev/null +++ b/.changes/unreleased/NOTES-20250319-150717.yaml @@ -0,0 +1,8 @@ +kind: NOTES +body: This alpha pre-release contains an initial implementation for managed resource identity, which can used with Terraform v1.12.0-alpha20250312, + to store and read identity data during plan and apply workflows. A managed resource identity can be used by defining an identity schema in the + `resource.Identity` field. Once the identity schema is defined, you can read and store identity data in the new IdentityData struct that is available + via the new `Identity()` method on ResourceData and ResourceDiff structs. +time: 2025-03-19T15:07:17.690255+01:00 +custom: + Issue: "1444" diff --git a/go.mod b/go.mod index 92ee6894c37..c6b9ee7f03d 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-exec v0.22.0 github.com/hashicorp/terraform-json v0.24.0 - github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250313132633-b799d5c93127 + github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/mitchellh/copystructure v1.2.0 github.com/mitchellh/go-testing-interface v1.14.1 diff --git a/go.sum b/go.sum index 04cdc15a342..70912916d6d 100644 --- a/go.sum +++ b/go.sum @@ -82,6 +82,8 @@ github.com/hashicorp/terraform-json v0.24.0 h1:rUiyF+x1kYawXeRth6fKFm/MdfBS6+lW4 github.com/hashicorp/terraform-json v0.24.0/go.mod h1:Nfj5ubo9xbu9uiAoZVBsNOjvNKB66Oyrvtit74kC7ow= github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250313132633-b799d5c93127 h1:V7RFaz+LWQY/S8tiWLuVFQMigEoZOpCF2vv7FW83vVw= github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250313132633-b799d5c93127/go.mod h1:MfDwS/KnIy2QzCwdRtuqIjZ23gpYa9Vm+Z8cFpx8qtU= +github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1 h1:/IZFNUEafGnJGXRe2iNQQ+vtzEw/5qiD+gOxkFrNbi4= +github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1/go.mod h1:Tf2HngbyKvovAlGXgBOVGm3EDvbNaN/StUaTXwrej4o= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.2.4 h1:JXu/zHB2Ymg/TGVCRu10XqNa4Sh2bWcqCNyKWjnCPJA= From 7821f306b20f7901e607c852143e52c2ad552516 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 20 Mar 2025 04:07:51 -0400 Subject: [PATCH 15/53] chore: Update alpha version of TF mentioned in changelog (#1451) --- .changes/unreleased/NOTES-20250319-150717.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changes/unreleased/NOTES-20250319-150717.yaml b/.changes/unreleased/NOTES-20250319-150717.yaml index c4807b7d8a6..dc1e7d8c4c3 100644 --- a/.changes/unreleased/NOTES-20250319-150717.yaml +++ b/.changes/unreleased/NOTES-20250319-150717.yaml @@ -1,5 +1,5 @@ kind: NOTES -body: This alpha pre-release contains an initial implementation for managed resource identity, which can used with Terraform v1.12.0-alpha20250312, +body: This alpha pre-release contains an initial implementation for managed resource identity, which can used with Terraform v1.12.0-alpha20250319, to store and read identity data during plan and apply workflows. A managed resource identity can be used by defining an identity schema in the `resource.Identity` field. Once the identity schema is defined, you can read and store identity data in the new IdentityData struct that is available via the new `Identity()` method on ResourceData and ResourceDiff structs. From c8df4e6ef68036f1eee5a4c7e45ae13e8c61b2fe Mon Sep 17 00:00:00 2001 From: Ansgar Mertens Date: Thu, 20 Mar 2025 15:19:55 +0100 Subject: [PATCH 16/53] Prepare 2.37.0-alpha.1 release (#1450) * Handle pre-releases in Github Action updating meta.go * update CONTRIBUTING.md to reflect the current reality --- .github/CONTRIBUTING.md | 8 +++----- .github/workflows/release.yml | 25 ++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index fbbdd59452b..8ad9019cd8b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -339,10 +339,6 @@ This section is dedicated to the maintainers of this project. ### Releases -Before running a release: - -- **`meta/meta.go`**: The versions must be appropriately updated. - To cut a release, go to the repository in GitHub and click on the `Actions` tab. Select the `Release` workflow on the left-hand menu. @@ -352,4 +348,6 @@ Click on the `Run workflow` button. Select the branch to cut the release from (default is main). Input the `Release version number` which is the Semantic Release number including -the `v` prefix (i.e. `v1.4.0`) and click `Run workflow` to kickoff the release. +the `v` prefix (i.e. `v1.4.0` or `v1.4.0-alpha.1`) and click `Run workflow` to kickoff the release. + +The (deprecated) version information in `meta/meta.go` will be updated automatically and a commit will be pushed. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b59bd7cdd31..14ee4fc95e1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,9 +20,26 @@ jobs: runs-on: ubuntu-latest outputs: version: ${{ steps.changelog-version.outputs.version }} + version_only: ${{ steps.changelog-version.outputs.version_only }} + prerelease: ${{ steps.changelog-version.outputs.prerelease }} steps: - id: changelog-version - run: echo "version=$(echo "${{ inputs.versionNumber }}" | cut -c 2-)" >> "$GITHUB_OUTPUT" + run: | + version="${{ inputs.versionNumber }}" + version="${version#v}" # Remove leading "v" if present + version_only="${version%%-*}" + prerelease="${version#*-}" + + # If there's no dash, set prerelease to empty + if [ "$version" = "$version_only" ]; then + prerelease="" + fi + + { + echo "version=$version" + echo "version_only=$version_only" + echo "prerelease=$prerelease" + } >> "$GITHUB_OUTPUT" changelog: needs: [ changelog-version, meta-version ] @@ -70,8 +87,10 @@ jobs: # Avoid persisting GITHUB_TOKEN credentials as they take priority over our service account PAT for `git push` operations # More details: https://github.com/actions/checkout/blob/b4626ce19ce1106186ddf9bb20e706842f11a7c3/adrs/0153-checkout-v2.md#persist-credentials persist-credentials: false - - name: Update meta package SDKVersion - run: sed -i "s/var SDKVersion =.*/var SDKVersion = \"${{ needs.changelog-version.outputs.version }}\"/" meta/meta.go + - name: Update meta package SDKVersion and SDKPrerelease + run: | + sed -i "s/var SDKVersion =.*/var SDKVersion = \"${{ needs.changelog-version.outputs.version_only }}\"/" meta/meta.go + sed -i "s/var SDKPrerelease =.*/var SDKPrerelease = \"${{ needs.changelog-version.outputs.prerelease }}\"/" meta/meta.go - name: Git push meta run: | git config --global user.name "${{ vars.TF_DEVEX_CI_COMMIT_AUTHOR }}" From d7706818157ad7c8121aa7b8a7b66df68e9b8e13 Mon Sep 17 00:00:00 2001 From: hc-github-team-tf-provider-devex Date: Thu, 20 Mar 2025 14:20:50 +0000 Subject: [PATCH 17/53] Update meta package SDKVersion --- meta/meta.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meta/meta.go b/meta/meta.go index 0a928c8b042..39a187a1af7 100644 --- a/meta/meta.go +++ b/meta/meta.go @@ -17,7 +17,7 @@ import ( // // Deprecated: Use Go standard library [runtime/debug] package build information // instead. -var SDKVersion = "2.36.1" +var SDKVersion = "2.37.0" // A pre-release marker for the version. If this is "" (empty string) // then it means that it is a final release. Otherwise, this is a pre-release @@ -25,7 +25,7 @@ var SDKVersion = "2.36.1" // // Deprecated: Use Go standard library [runtime/debug] package build information // instead. -var SDKPrerelease = "" +var SDKPrerelease = "alpha.1" // SemVer is an instance of version.Version. This has the secondary // benefit of verifying during tests and init time that our version is a From fe25e82bcf8d6744ff336c66d3cc35164eb15271 Mon Sep 17 00:00:00 2001 From: hc-github-team-tf-provider-devex Date: Thu, 20 Mar 2025 14:21:08 +0000 Subject: [PATCH 18/53] Update changelog --- .changes/2.37.0-alpha.1.md | 7 +++++++ .changes/unreleased/NOTES-20250317-152749.yaml | 7 ------- .changes/unreleased/NOTES-20250319-150717.yaml | 8 -------- CHANGELOG.md | 7 +++++++ 4 files changed, 14 insertions(+), 15 deletions(-) create mode 100644 .changes/2.37.0-alpha.1.md delete mode 100644 .changes/unreleased/NOTES-20250317-152749.yaml delete mode 100644 .changes/unreleased/NOTES-20250319-150717.yaml diff --git a/.changes/2.37.0-alpha.1.md b/.changes/2.37.0-alpha.1.md new file mode 100644 index 00000000000..11fdb522b6a --- /dev/null +++ b/.changes/2.37.0-alpha.1.md @@ -0,0 +1,7 @@ +## 2.37.0-alpha.1 (March 20, 2025) + +NOTES: + +* all: This Go module has been updated to Go 1.23 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.23 release notes](https://go.dev/doc/go1.23) before upgrading. Any consumers building on earlier Go versions may experience errors. ([#1445](https://github.com/hashicorp/terraform-plugin-sdk/issues/1445)) +* This alpha pre-release contains an initial implementation for managed resource identity, which can used with Terraform v1.12.0-alpha20250319, to store and read identity data during plan and apply workflows. A managed resource identity can be used by defining an identity schema in the `resource.Identity` field. Once the identity schema is defined, you can read and store identity data in the new IdentityData struct that is available via the new `Identity()` method on ResourceData and ResourceDiff structs. ([#1444](https://github.com/hashicorp/terraform-plugin-sdk/issues/1444)) + diff --git a/.changes/unreleased/NOTES-20250317-152749.yaml b/.changes/unreleased/NOTES-20250317-152749.yaml deleted file mode 100644 index d9faaebe4ab..00000000000 --- a/.changes/unreleased/NOTES-20250317-152749.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: NOTES -body: 'all: This Go module has been updated to Go 1.23 per the [Go support policy](https://go.dev/doc/devel/release#policy). - It is recommended to review the [Go 1.23 release notes](https://go.dev/doc/go1.23) - before upgrading. Any consumers building on earlier Go versions may experience errors.' -time: 2025-03-17T15:27:49.758739-04:00 -custom: - Issue: "1445" diff --git a/.changes/unreleased/NOTES-20250319-150717.yaml b/.changes/unreleased/NOTES-20250319-150717.yaml deleted file mode 100644 index dc1e7d8c4c3..00000000000 --- a/.changes/unreleased/NOTES-20250319-150717.yaml +++ /dev/null @@ -1,8 +0,0 @@ -kind: NOTES -body: This alpha pre-release contains an initial implementation for managed resource identity, which can used with Terraform v1.12.0-alpha20250319, - to store and read identity data during plan and apply workflows. A managed resource identity can be used by defining an identity schema in the - `resource.Identity` field. Once the identity schema is defined, you can read and store identity data in the new IdentityData struct that is available - via the new `Identity()` method on ResourceData and ResourceDiff structs. -time: 2025-03-19T15:07:17.690255+01:00 -custom: - Issue: "1444" diff --git a/CHANGELOG.md b/CHANGELOG.md index 73678077836..4c325b4625a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 2.37.0-alpha.1 (March 20, 2025) + +NOTES: + +* all: This Go module has been updated to Go 1.23 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.23 release notes](https://go.dev/doc/go1.23) before upgrading. Any consumers building on earlier Go versions may experience errors. ([#1445](https://github.com/hashicorp/terraform-plugin-sdk/issues/1445)) +* This alpha pre-release contains an initial implementation for managed resource identity, which can used with Terraform v1.12.0-alpha20250319, to store and read identity data during plan and apply workflows. A managed resource identity can be used by defining an identity schema in the `resource.Identity` field. Once the identity schema is defined, you can read and store identity data in the new IdentityData struct that is available via the new `Identity()` method on ResourceData and ResourceDiff structs. ([#1444](https://github.com/hashicorp/terraform-plugin-sdk/issues/1444)) + ## 2.36.1 (February 19, 2025) NOTES: From b8445ee6b61bd574c02047f3686c74e4d9b4348a Mon Sep 17 00:00:00 2001 From: Ansgar Mertens Date: Fri, 21 Mar 2025 09:54:08 +0100 Subject: [PATCH 19/53] error if there's no identity schema (or an empty one) defined when it should be (#1452) * error if there's no identity schema (or an empty one) defined when it should be * add test for new diagnostics returned by GetResourceIdentitySchemas * add test for new diagnostics returned by ReadResource, PlanResourceChange, and ApplyResourceChange also adds identity test for ApplyResourceChange that didn't exist yet --- helper/schema/core_schema.go | 21 +- helper/schema/grpc_provider.go | 58 ++- helper/schema/grpc_provider_test.go | 546 +++++++++++++++++++++++++++- 3 files changed, 604 insertions(+), 21 deletions(-) diff --git a/helper/schema/core_schema.go b/helper/schema/core_schema.go index e01ca497135..388aafd1c12 100644 --- a/helper/schema/core_schema.go +++ b/helper/schema/core_schema.go @@ -374,21 +374,26 @@ func (r *Resource) coreConfigSchema() *configschema.Block { return schemaMap(r.SchemaMap()).CoreConfigSchema() } -func (r *Resource) CoreIdentitySchema() *configschema.Block { - block := r.coreIdentitySchema() +func (r *Resource) CoreIdentitySchema() (*configschema.Block, error) { + block, err := r.coreIdentitySchema() + + if err != nil { + return nil, err + } if block.Attributes == nil { - // TODO: we should error instead and callers should handle the error appropriately - // and error would hint at an invalid provider implementation - block.Attributes = map[string]*configschema.Attribute{} + return nil, fmt.Errorf("identity schema must have at least one attribute") } - return block + return block, nil } -func (r *Resource) coreIdentitySchema() *configschema.Block { +func (r *Resource) coreIdentitySchema() (*configschema.Block, error) { + if r.Identity == nil || r.Identity.Schema == nil { + return nil, fmt.Errorf("resource does not have an identity schema") + } // while there is schemaMapWithIdentity, we don't need to use it here // as we're only interested in the existing CoreConfigSchema() method // to convert our schema - return schemaMap(r.Identity.Schema).CoreConfigSchema() + return schemaMap(r.Identity.Schema).CoreConfigSchema(), nil } diff --git a/helper/schema/grpc_provider.go b/helper/schema/grpc_provider.go index 0071d1c8d6b..843314f3ba6 100644 --- a/helper/schema/grpc_provider.go +++ b/helper/schema/grpc_provider.go @@ -90,9 +90,16 @@ func (s *GRPCProviderServer) GetResourceIdentitySchemas(ctx context.Context, req logging.HelperSchemaTrace(ctx, "Found resource identity type", map[string]interface{}{logging.KeyResourceType: typ}) if res.Identity != nil { + idschema, err := res.CoreIdentitySchema() + + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("getting identity schema failed for resource '%s': %w", typ, err)) + return resp, nil + } + resp.IdentitySchemas[typ] = &tfprotov5.ResourceIdentitySchema{ Version: res.Identity.Version, - IdentityAttributes: convert.ConfigIdentitySchemaToProto(ctx, res.CoreIdentitySchema()), + IdentityAttributes: convert.ConfigIdentitySchemaToProto(ctx, idschema), } } } @@ -110,12 +117,16 @@ func (s *GRPCProviderServer) UpgradeResourceIdentity(ctx context.Context, req *t resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("unknown resource type: %s", req.TypeName)) return resp, nil } - schemaBlock := s.getResourceIdentitySchemaBlock(req.TypeName) + + schemaBlock, err := s.getResourceIdentitySchemaBlock(req.TypeName) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) + return resp, nil + } version := req.Version jsonMap := map[string]interface{}{} - var err error switch { // if there's a JSON state, we need to decode it. @@ -258,7 +269,7 @@ func (s *GRPCProviderServer) getResourceSchemaBlock(name string) *configschema.B return res.CoreConfigSchema() } -func (s *GRPCProviderServer) getResourceIdentitySchemaBlock(name string) *configschema.Block { +func (s *GRPCProviderServer) getResourceIdentitySchemaBlock(name string) (*configschema.Block, error) { res := s.provider.ResourcesMap[name] return res.CoreIdentitySchema() } @@ -826,7 +837,12 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re // convert req.CurrentIdentity to flat map identity structure // Step 1: Turn JSON into cty.Value based on schema - identityBlock := s.getResourceIdentitySchemaBlock(req.TypeName) // TODO: handle error if no schema exists (and after we add the error) + identityBlock, err := s.getResourceIdentitySchemaBlock(req.TypeName) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("getting identity schema failed for resource '%s': %w", req.TypeName, err)) + return resp, nil + } + identityVal, err := msgpack.Unmarshal(req.CurrentIdentity.IdentityData.MsgPack, identityBlock.ImpliedType()) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) @@ -901,7 +917,11 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re } if newInstanceState.Identity != nil { - identityBlock := s.getResourceIdentitySchemaBlock(req.TypeName) // TODO: handle error if no schema exists (and after we add the error) + identityBlock, err := s.getResourceIdentitySchemaBlock(req.TypeName) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("getting identity schema failed for resource '%s': %w", req.TypeName, err)) + return resp, nil + } newIdentityVal, err := hcl2shim.HCL2ValueFromFlatmap(newInstanceState.Identity, identityBlock.ImpliedType()) if err != nil { @@ -1036,7 +1056,12 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot if req.PriorIdentity != nil && req.PriorIdentity.IdentityData != nil { // convert req.PriorIdentity to flat map identity structure // Step 1: Turn JSON into cty.Value based on schema - identityBlock := s.getResourceIdentitySchemaBlock(req.TypeName) // TODO: handle error if no schema exists (and after we add the error) + identityBlock, err := s.getResourceIdentitySchemaBlock(req.TypeName) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("getting identity schema failed for resource '%s': %w", req.TypeName, err)) + return resp, nil + } + identityVal, err := msgpack.Unmarshal(req.PriorIdentity.IdentityData.MsgPack, identityBlock.ImpliedType()) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) @@ -1226,7 +1251,11 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot // TODO: if schema defines identity, we should error if there's none written to newInstanceState if res.Identity != nil { - identityBlock := s.getResourceIdentitySchemaBlock(req.TypeName) // TODO: handle error if no schema exists (and after we add the error) + identityBlock, err := s.getResourceIdentitySchemaBlock(req.TypeName) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("getting identity schema failed for resource '%s': %w", req.TypeName, err)) + return resp, nil + } newIdentityVal, err := hcl2shim.HCL2ValueFromFlatmap(diff.Identity, identityBlock.ImpliedType()) if err != nil { @@ -1300,7 +1329,12 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro if req.PlannedIdentity != nil && req.PlannedIdentity.IdentityData != nil { // convert req.PriorIdentity to flat map identity structure // Step 1: Turn JSON into cty.Value based on schema - identityBlock := s.getResourceIdentitySchemaBlock(req.TypeName) // TODO: handle error if no schema exists (and after we add the error) + identityBlock, err := s.getResourceIdentitySchemaBlock(req.TypeName) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("getting identity schema failed for resource '%s': %w", req.TypeName, err)) + return resp, nil + } + identityVal, err := msgpack.Unmarshal(req.PlannedIdentity.IdentityData.MsgPack, identityBlock.ImpliedType()) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) @@ -1443,7 +1477,11 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro // TODO: if schema defines identity, we should error if there's none written to newInstanceState if res.Identity != nil { - identityBlock := s.getResourceIdentitySchemaBlock(req.TypeName) // TODO: handle error if no schema exists (and after we add the error) + identityBlock, err := s.getResourceIdentitySchemaBlock(req.TypeName) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("getting identity schema failed for resource '%s': %w", req.TypeName, err)) + return resp, nil + } newIdentityVal, err := hcl2shim.HCL2ValueFromFlatmap(newInstanceState.Identity, identityBlock.ImpliedType()) if err != nil { diff --git a/helper/schema/grpc_provider_test.go b/helper/schema/grpc_provider_test.go index 274ebc986c9..95e70999062 100644 --- a/helper/schema/grpc_provider_test.go +++ b/helper/schema/grpc_provider_test.go @@ -3421,6 +3421,47 @@ func TestGRPCProviderServerGetResourceIdentitySchemas(t *testing.T) { }, }, }, + "no identity schema": { + Provider: &Provider{ + ResourcesMap: map[string]*Resource{ + "test_resource1": { + Identity: &ResourceIdentity{ + Version: 1, + }, + }, + }, + }, + Expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{}, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "getting identity schema failed for resource 'test_resource1': resource does not have an identity schema", + }, + }, + }, + }, + "empty identity schema": { + Provider: &Provider{ + ResourcesMap: map[string]*Resource{ + "test_resource1": { + Identity: &ResourceIdentity{ + Version: 1, + Schema: map[string]*Schema{}, + }, + }, + }, + }, + Expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{}, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "getting identity schema failed for resource 'test_resource1': identity schema must have at least one attribute", + }, + }, + }, + }, } for name, testCase := range testCases { @@ -3515,7 +3556,13 @@ func TestUpgradeResourceIdentity_jsonState(t *testing.T) { t.Fatal("error") } - val, err := msgpack.Unmarshal(resp.UpgradedIdentity.IdentityData.MsgPack, r.CoreIdentitySchema().ImpliedType()) + idschema, err := r.CoreIdentitySchema() + + if err != nil { + t.Fatal(err) + } + + val, err := msgpack.Unmarshal(resp.UpgradedIdentity.IdentityData.MsgPack, idschema.ImpliedType()) if err != nil { t.Fatal(err) } @@ -3593,7 +3640,12 @@ func TestUpgradeResourceIdentity_removedAttr(t *testing.T) { t.Fatal("error") } - val, err := msgpack.Unmarshal(resp.UpgradedIdentity.IdentityData.MsgPack, r.CoreIdentitySchema().ImpliedType()) + idschema, err := r.CoreIdentitySchema() + if err != nil { + t.Fatal(err) + } + + val, err := msgpack.Unmarshal(resp.UpgradedIdentity.IdentityData.MsgPack, idschema.ImpliedType()) if err != nil { t.Fatal(err) } @@ -3652,7 +3704,12 @@ func TestUpgradeResourceIdentity_jsonStateBigInt(t *testing.T) { t.Fatal("error") } - val, err := msgpack.Unmarshal(resp.UpgradedIdentity.IdentityData.MsgPack, r.CoreIdentitySchema().ImpliedType()) + idschema, err := r.CoreIdentitySchema() + if err != nil { + t.Fatal(err) + } + + val, err := msgpack.Unmarshal(resp.UpgradedIdentity.IdentityData.MsgPack, idschema.ImpliedType()) if err != nil { t.Fatal(err) } @@ -5115,6 +5172,97 @@ func TestReadResource(t *testing.T) { }, }, }, + "no-identity-schema": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 1, + Identity: &ResourceIdentity{ + Version: 1, + }, + }, + }, + }), + req: &tfprotov5.ReadResourceRequest{ + TypeName: "test", + CurrentIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "instance_id": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "instance_id": cty.StringVal("test-id"), + }), + ), + }, + }, + CurrentState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("test-id"), + }), + ), + }, + }, + expected: &tfprotov5.ReadResourceResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "getting identity schema failed for resource 'test': resource does not have an identity schema", + }, + }, + }, + }, + "empty-identity": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 1, + Identity: &ResourceIdentity{ + Version: 1, + Schema: map[string]*Schema{}, + }, + }, + }, + }), + req: &tfprotov5.ReadResourceRequest{ + TypeName: "test", + CurrentIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "instance_id": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "instance_id": cty.StringVal("test-id"), + }), + ), + }, + }, + CurrentState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("test-id"), + }), + ), + }, + }, + expected: &tfprotov5.ReadResourceResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "getting identity schema failed for resource 'test': identity schema must have at least one attribute", + }, + }, + }, + }, "deferred-response-unknown-val": { server: NewGRPCProviderServer(&Provider{ // Deferred response will skip read function and return current state @@ -5586,6 +5734,163 @@ func TestPlanResourceChange(t *testing.T) { }, }, }, + "no identity schema": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 4, + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + Optional: true, + }, + }, + Identity: &ResourceIdentity{ + Version: 1, + }, + }, + }, + }), + req: &tfprotov5.PlanResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "foo": cty.Number, + }), + cty.NullVal( + cty.Object(map[string]cty.Type{ + "foo": cty.Number, + }), + ), + ), + }, + ProposedNewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.Number, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.NullVal(cty.Number), + }), + ), + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.Number, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "foo": cty.NullVal(cty.Number), + }), + ), + }, + PriorIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "name": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("test-name"), + }), + ), + }, + }, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "getting identity schema failed for resource 'test': resource does not have an identity schema", + }, + }, + UnsafeToUseLegacyTypeSystem: true, + }, + }, + "empty identity schema": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 4, + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + Optional: true, + }, + }, + Identity: &ResourceIdentity{ + Version: 1, + Schema: map[string]*Schema{}, + }, + }, + }, + }), + req: &tfprotov5.PlanResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "foo": cty.Number, + }), + cty.NullVal( + cty.Object(map[string]cty.Type{ + "foo": cty.Number, + }), + ), + ), + }, + ProposedNewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.Number, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.NullVal(cty.Number), + }), + ), + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.Number, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "foo": cty.NullVal(cty.Number), + }), + ), + }, + PriorIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "name": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("test-name"), + }), + ), + }, + }, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "getting identity schema failed for resource 'test': identity schema must have at least one attribute", + }, + }, + UnsafeToUseLegacyTypeSystem: true, + }, + }, "basic-plan-EnableLegacyTypeSystemPlanErrors": { server: NewGRPCProviderServer(&Provider{ ResourcesMap: map[string]*Resource{ @@ -6506,6 +6811,241 @@ func TestApplyResourceChange(t *testing.T) { UnsafeToUseLegacyTypeSystem: true, }, }, + "create: identity returned in ApplyResourceChangeResponse": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 4, + CreateContext: func(_ context.Context, rd *ResourceData, _ interface{}) diag.Diagnostics { + rd.SetId("baz") + identity, err := rd.Identity() + if err != nil { + t.Fatal(err) + } + err = identity.Set("ident", "bazz") + if err != nil { + t.Fatal(err) + } + return nil + }, + Schema: map[string]*Schema{}, + Identity: &ResourceIdentity{ + Version: 1, + Schema: map[string]*Schema{ + "ident": { + Type: TypeString, + RequiredForImport: true, + }, + }, + }, + }, + }, + }), + req: &tfprotov5.ApplyResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{}), + cty.NullVal( + cty.Object(map[string]cty.Type{}), + ), + ), + }, + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + }), + ), + }, + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "ident": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "ident": cty.UnknownVal(cty.String), + }), + ), + }, + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + }), + ), + }, + }, + expected: &tfprotov5.ApplyResourceChangeResponse{ + NewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("baz"), + }), + ), + }, + Private: []uint8(`{"schema_version":"4"}`), + UnsafeToUseLegacyTypeSystem: true, + NewIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "ident": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "ident": cty.StringVal("bazz"), + }), + ), + }, + }, + }, + }, + "create: no identity schema diag in ApplyResourceChangeResponse": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 4, + Schema: map[string]*Schema{}, + Identity: &ResourceIdentity{ + Version: 1, + }, + }, + }, + }), + req: &tfprotov5.ApplyResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{}), + cty.NullVal( + cty.Object(map[string]cty.Type{}), + ), + ), + }, + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + }), + ), + }, + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "ident": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "ident": cty.UnknownVal(cty.String), + }), + ), + }, + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + }), + ), + }, + }, + expected: &tfprotov5.ApplyResourceChangeResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "getting identity schema failed for resource 'test': resource does not have an identity schema", + }, + }, + NewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal(cty.DynamicPseudoType, cty.NullVal(cty.DynamicPseudoType)), + }, + }, + }, + "create: empty identity schema diag in ApplyResourceChangeResponse": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 4, + Schema: map[string]*Schema{}, + Identity: &ResourceIdentity{ + Version: 1, + Schema: map[string]*Schema{}, + }, + }, + }, + }), + req: &tfprotov5.ApplyResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{}), + cty.NullVal( + cty.Object(map[string]cty.Type{}), + ), + ), + }, + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + }), + ), + }, + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "ident": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "ident": cty.UnknownVal(cty.String), + }), + ), + }, + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + }), + ), + }, + }, + expected: &tfprotov5.ApplyResourceChangeResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "getting identity schema failed for resource 'test': identity schema must have at least one attribute", + }, + }, + NewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal(cty.DynamicPseudoType, cty.NullVal(cty.DynamicPseudoType)), + }, + }, + }, } for name, testCase := range testCases { From 03c2baa22d8f4cf85518de3269385a6d59eb3c43 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 11:10:46 -0400 Subject: [PATCH 20/53] build(deps): bump github.com/golang-jwt/jwt/v4 in /tools (#1454) Bumps [github.com/golang-jwt/jwt/v4](https://github.com/golang-jwt/jwt) from 4.5.1 to 4.5.2. - [Release notes](https://github.com/golang-jwt/jwt/releases) - [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md) - [Commits](https://github.com/golang-jwt/jwt/compare/v4.5.1...v4.5.2) --- updated-dependencies: - dependency-name: github.com/golang-jwt/jwt/v4 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tools/go.mod | 2 +- tools/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/go.mod b/tools/go.mod index 7ba09d85002..e4ab24aa326 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -17,7 +17,7 @@ require ( github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/go-openapi/errors v0.20.2 // indirect github.com/go-openapi/strfmt v0.21.3 // indirect - github.com/golang-jwt/jwt/v4 v4.5.1 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-github/v45 v45.2.0 // indirect github.com/google/go-github/v53 v53.0.0 // indirect diff --git a/tools/go.sum b/tools/go.sum index f5e51f9d857..d5a7ac1f838 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -96,8 +96,8 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= -github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= From f4c4385e724a6eefaee15601b1475caba4cc9a1a Mon Sep 17 00:00:00 2001 From: "hashicorp-tsccr[bot]" <129506189+hashicorp-tsccr[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 11:23:20 -0400 Subject: [PATCH 21/53] Result of tsccr-helper -log-level=info gha update -latest .github/ (#1456) Co-authored-by: hashicorp-tsccr[bot] --- .github/workflows/ci-go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index 43629cd3484..0cec866afaa 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -21,7 +21,7 @@ jobs: with: go-version-file: 'go.mod' - run: go mod download - - uses: golangci/golangci-lint-action@2226d7cb06a077cd73e56eedd38eecad18e5d837 # v6.5.0 + - uses: golangci/golangci-lint-action@4696ba8babb6127d732c3c6dde519db15edab9ea # v6.5.1 with: version: latest terraform-provider-corner-tfprotov5: From e45d23ad87666c766744130107e54f69369869b7 Mon Sep 17 00:00:00 2001 From: Rain Kwan <91649079+rainkwan@users.noreply.github.com> Date: Wed, 26 Mar 2025 11:23:08 -0400 Subject: [PATCH 22/53] Resource Identity: Add internal validation (#1447) * Added validation testing to InternalIdentityValidate and testing to resource_test.go * Consolidated error catching for identity attributes in the resource schema * Add error test for identity attributes in the resource schema * Checks if length of schema is 0 as well as if it is nil for error handling --- helper/schema/provider.go | 5 + helper/schema/resource.go | 132 ++++++++++++ helper/schema/resource_test.go | 373 +++++++++++++++++++++++++++++++++ helper/schema/schema.go | 7 + helper/schema/schema_test.go | 20 ++ 5 files changed, 537 insertions(+) diff --git a/helper/schema/provider.go b/helper/schema/provider.go index 45f1e0d466b..ad258517eac 100644 --- a/helper/schema/provider.go +++ b/helper/schema/provider.go @@ -218,6 +218,11 @@ func (p *Provider) InternalValidate() error { } for k, r := range p.ResourcesMap { + if r.Identity != nil { + if err := r.Identity.InternalIdentityValidate(); err != nil { + validationErrors = append(validationErrors, fmt.Errorf("resource %s identity: %s", k, err)) + } + } if err := r.InternalValidate(nil, true); err != nil { validationErrors = append(validationErrors, fmt.Errorf("resource %s: %s", k, err)) } diff --git a/helper/schema/resource.go b/helper/schema/resource.go index b10a51d3c34..ac6db21386b 100644 --- a/helper/schema/resource.go +++ b/helper/schema/resource.go @@ -1482,3 +1482,135 @@ func RemoveFromState(d *ResourceData, _ interface{}) error { d.SetId("") return nil } + +// Internal validation of provider implementation +func (r *ResourceIdentity) InternalIdentityValidate() error { + if r == nil { + return fmt.Errorf(`The resource identity is empty`) + } + + if len(r.Schema) == 0 { + return fmt.Errorf(`The resource identity schema is empty`) + } + + for k, v := range r.Schema { + if !v.OptionalForImport && !v.RequiredForImport { + return fmt.Errorf(`OptionalForImport or RequiredForImport must be set for resource identity`) + } + if v.OptionalForImport && v.RequiredForImport { + return fmt.Errorf(`OptionalForImport or RequiredForImport must be set for resource identity, not both`) + } + + if v.Type == TypeMap { + return fmt.Errorf(`TypeMap is not valid for resource identity`) + } + if v.Type == TypeSet { + return fmt.Errorf(`TypeSet is not valid for resource identity`) + } + if v.Type == typeObject { + return fmt.Errorf(`TypeObject is not valid for resource identity`) + } + if v.Type == TypeInvalid { + return fmt.Errorf(`TypeInvalid is not valid for resource identity`) + } + + if v.Type == TypeList { + if v.Elem != nil { + if v.Elem == TypeMap { + return fmt.Errorf(`TypeMap is not valid for resource identity element type`) + } + if v.Elem == TypeSet { + return fmt.Errorf(`TypeSet is not valid for resource identity element type`) + } + if v.Elem == typeObject { + return fmt.Errorf(`TypeObject is not valid for resource identity element type`) + } + if v.Elem == TypeInvalid { + return fmt.Errorf(`TypeInvalid is not valid for resource identity element type`) + } + } + } + + if v.ForceNew { + return fmt.Errorf(`ForceNew is not used in resource identity`) + } + if v.Required { + return fmt.Errorf(`Required is not used in resource identity`) + } + if v.Optional { + return fmt.Errorf(`Optional is not used in resource identity`) + } + if v.WriteOnly { + return fmt.Errorf(`WriteOnly is not used in resource identity`) + } + if v.Computed { + return fmt.Errorf(`Computed is not used in resource identity`) + } + if v.Sensitive { + return fmt.Errorf(`Sensitive is not used in resource identity`) + } + if v.DiffSuppressOnRefresh { + return fmt.Errorf(`DiffSuppressOnRefresh is not used in resource identity`) + } + if v.Deprecated != "" { + return fmt.Errorf(`Deprecated is not used in resource identity`) + } + if len(v.RequiredWith) > 0 { + return fmt.Errorf(`RequiredWith is not used in resource identity`) + } + if len(v.ComputedWhen) > 0 { + return fmt.Errorf(`ComputedWhen is not used in resource identity`) + } + if len(v.AtLeastOneOf) > 0 { + return fmt.Errorf("%s: AtLeastOneOf is for configurable attributes,"+ + "there's nothing to configure for resource identity", k) + } + if len(v.ConflictsWith) > 0 { + return fmt.Errorf("%s: ConflictsWith is for configurable attributes,"+ + "there's nothing to configure for resource identity", k) + } + if v.Default != nil { + return fmt.Errorf("%s: Default is for configurable attributes,"+ + "there's nothing to configure for resource identity", k) + } + if v.DefaultFunc != nil { + return fmt.Errorf("%s: DefaultFunc is for configurable attributes,"+ + "there's nothing to configure for resource identity", k) + } + if v.DiffSuppressFunc != nil { + return fmt.Errorf("%s: DiffSuppressFunc is for suppressing differences"+ + " between config and state representation. "+ + "There is no config for resource identity, nothing to compare.", k) + } + if len(v.ExactlyOneOf) > 0 { + return fmt.Errorf("%s: ExactlyOneOf is for configurable attributes,"+ + "there's nothing to configure for resource identity", k) + } + if v.InputDefault != "" { + return fmt.Errorf("%s: InputDefault is for configurable attributes,"+ + "there's nothing to configure for resource identity", k) + } + if v.MaxItems > 0 { + return fmt.Errorf("%s: MaxItems is for configurable attributes,"+ + "there's nothing to configure for resource identity", k) + } + if v.MinItems > 0 { + return fmt.Errorf("%s: MinItems is for configurable attributes,"+ + "there's nothing to configure for resource identity", k) + } + if v.StateFunc != nil { + return fmt.Errorf("%s: StateFunc is extraneous, "+ + "value should just be changed before setting for resource identity", k) + } + if v.ValidateFunc != nil { + return fmt.Errorf("%s: ValidateFunc is for validating user input, "+ + "there's nothing to validate for resource identity", k) + } + if v.ValidateDiagFunc != nil { + return fmt.Errorf("%s: ValidateDiagFunc is for validating user input, "+ + "there's nothing to validate for resource identity", k) + } + } + + return nil +} diff --git a/helper/schema/resource_test.go b/helper/schema/resource_test.go index 447338e6d52..cf64a6e6045 100644 --- a/helper/schema/resource_test.go +++ b/helper/schema/resource_test.go @@ -1659,3 +1659,376 @@ func TestResource_ContextTimeout(t *testing.T) { t.Fatal("context does not have timeout") } } + +func TestResourceInternalIdentityValidate(t *testing.T) { + cases := map[string]struct { + In *ResourceIdentity + Err bool + }{ + "nil": { + nil, + true, + }, + + "schema is nil": { + &ResourceIdentity{}, + true, + }, + + "OptionalForImport and RequiredForImport both false": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + OptionalForImport: false, + RequiredForImport: false, + }, + }, + }, + true, + }, + + "OptionalForImport and RequiredForImport both true": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + OptionalForImport: true, + RequiredForImport: true, + }, + }, + }, + true, + }, + + "TypeMap is not valid": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": {Type: TypeMap, OptionalForImport: true}, + }, + }, + true, + }, + + "TypeSet is not valid": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": {Type: TypeSet, OptionalForImport: true}, + }, + }, + true, + }, + + "TypeObject is not valid": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": {Type: typeObject, OptionalForImport: true}, + }, + }, + true, + }, + + "TypeInvalid is not valid": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": {Type: TypeInvalid, OptionalForImport: true}, + }, + }, + true, + }, + + "TypeList contains TypeMap": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeList, Elem: TypeMap, OptionalForImport: true, + }, + }, + }, + true, + }, + + " TypeList contains TypeSet": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeList, + Elem: &Resource{ + Schema: map[string]*Schema{ + "bar": { + Type: TypeSet, + RequiredForImport: true, + }, + }, + }, + }, + }, + }, + true, + }, + + "TypeList contains TypeInvalid": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeList, Elem: TypeInvalid, OptionalForImport: true, + }, + }, + }, + true, + }, + + "ForceNew is set": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + ForceNew: true, + OptionalForImport: true, + }, + }, + }, + true, + }, + + "Optional is set": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + Optional: true, + OptionalForImport: true, + }, + }, + }, + true, + }, + + "Required is set": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + Required: true, + OptionalForImport: true, + }, + }, + }, + true, + }, + + "WriteOnly is set": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + WriteOnly: true, + OptionalForImport: true, + }, + }, + }, + true, + }, + + "Computed is set": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + Computed: true, + OptionalForImport: true, + }, + }, + }, + true, + }, + + "Deprecated is set": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + Deprecated: "deprecated", + OptionalForImport: true, + }, + }, + }, + true, + }, + + "Default is set": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + Default: 42, + OptionalForImport: true, + }, + }, + }, + true, + }, + + "MaxItems is set": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + MaxItems: 5, + OptionalForImport: true, + }, + }, + }, + true, + }, + + "MinItems is set": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + MinItems: 1, + OptionalForImport: true, + }, + }, + }, + true, + }, + + "DiffSuppressOnRefresh is set": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + DiffSuppressOnRefresh: true, + OptionalForImport: true, + }, + }, + }, + true, + }, + + "RequiredWith is set": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + RequiredWith: []string{"bar"}, + OptionalForImport: true, + }, + }, + }, + true, + }, + + "ComputedWhen is set": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + ComputedWhen: []string{"bar"}, + OptionalForImport: true, + }, + }, + }, + true, + }, + + "DefaultFunc is set": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + DefaultFunc: func() (interface{}, error) { return 42, nil }, + OptionalForImport: true, + }, + }, + }, + true, + }, + + "StateFunc is set": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + StateFunc: func(val interface{}) string { return "" }, + OptionalForImport: true, + }, + }, + }, + true, + }, + + "ValidateFunc is set": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + ValidateFunc: func(val interface{}, key string) (ws []string, es []error) { return nil, nil }, + OptionalForImport: true, + }, + }, + }, + true, + }, + + "AtLeastOneOf is set": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + AtLeastOneOf: []string{"bar"}, + OptionalForImport: true, + }, + }, + }, + true, + }, + + "ConflictsWith is set": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + ConflictsWith: []string{"bar"}, + OptionalForImport: true, + }, + }, + }, + true, + }, + + "Valid resource identity OptionalForImport": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, OptionalForImport: true}, + }, + }, + false, + }, + + "Valid resource identity RequiredorImport": { + &ResourceIdentity{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, RequiredForImport: true}, + }, + }, + false, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + err := tc.In.InternalIdentityValidate() + if err != nil && !tc.Err { + t.Fatalf("%s: expected validation to pass: %s", name, err) + } + if err == nil && tc.Err { + t.Fatalf("%s: expected validation to fail", name) + } + }) + } +} diff --git a/helper/schema/schema.go b/helper/schema/schema.go index 1d56ec37797..2861b4ad31a 100644 --- a/helper/schema/schema.go +++ b/helper/schema/schema.go @@ -972,6 +972,13 @@ func (m schemaMap) internalValidate(topSchemaMap schemaMap, attrsOnly bool) erro return fmt.Errorf("%s: WriteOnly cannot be set with ForceNew", k) } + if v.RequiredForImport { + return fmt.Errorf("%s: RequiredForImport is only valid for resource identity schemas", k) + } + if v.OptionalForImport { + return fmt.Errorf("%s: OptionalForImport is only valid for resource identity schemas", k) + } + computedOnly := v.Computed && !v.Optional switch v.ConfigMode { diff --git a/helper/schema/schema_test.go b/helper/schema/schema_test.go index 866e22ce816..bfb566e9210 100644 --- a/helper/schema/schema_test.go +++ b/helper/schema/schema_test.go @@ -5414,6 +5414,26 @@ func TestSchemaMap_InternalValidate(t *testing.T) { }, true, }, + "OptionalForImport returns error": { + map[string]*Schema{ + "foo": { + Type: TypeInt, + OptionalForImport: true, + Optional: true, + }, + }, + true, + }, + "RequiredForImport returns error": { + map[string]*Schema{ + "foo": { + Type: TypeString, + RequiredForImport: true, + Required: true, + }, + }, + true, + }, } for tn, tc := range cases { From 80327322ac35a5db215ea0e47824df11477faf92 Mon Sep 17 00:00:00 2001 From: Ansgar Mertens Date: Wed, 26 Mar 2025 17:19:16 +0100 Subject: [PATCH 23/53] TFECO-9157: Add tests for `identity_data.go` and switch to multiple field reader (#1453) * Start writing tests for identity_data.go * Switch to using multiple field readers in `IdentityData` This resembles the behaviour in `ResourceData` more closely. Specifically, this makes it possible to have number values in the source identity sent by Terraform that are encoded as strings. Before this commit this failed as the field writer does not allow to write strings for fields that are typed as integers in the schema, which is what we did to initialize the result. * add remaining test cases for GetOk and Set * address review feedback --- helper/schema/identity_data.go | 40 +- helper/schema/identity_data_test.go | 669 ++++++++++++++++++++++++++++ helper/schema/resource_data.go | 3 + 3 files changed, 694 insertions(+), 18 deletions(-) create mode 100644 helper/schema/identity_data_test.go diff --git a/helper/schema/identity_data.go b/helper/schema/identity_data.go index 9427d5a0be0..7107de1cf30 100644 --- a/helper/schema/identity_data.go +++ b/helper/schema/identity_data.go @@ -16,9 +16,9 @@ type IdentityData struct { schema map[string]*Schema // Don't set - once sync.Once - writer *MapFieldWriter - reader *MapFieldReader + once sync.Once + multiReader *MultiLevelFieldReader + setWriter *MapFieldWriter panicOnError bool } @@ -68,33 +68,37 @@ func (d *IdentityData) Set(key string, value interface{}) error { } } - err := d.writer.WriteField(strings.Split(key, "."), value) + err := d.setWriter.WriteField(strings.Split(key, "."), value) if err != nil { if d.panicOnError { panic(err) } else { - log.Printf("[ERROR] setting state: %s", err) + log.Printf("[ERROR] setting identity state: %s", err) } } return err } func (d *IdentityData) init() { - // re-use writers and readers to handle storing in flat map data structure - d.writer = &MapFieldWriter{Schema: d.schema} - for key, value := range d.raw { - err := d.writer.WriteField(strings.Split(key, "."), value) - if err != nil { - if d.panicOnError { - panic(err) - } else { - log.Printf("[ERROR] setting identity state: %s", err) - } + // Initialize the map for storing data set by the user + d.setWriter = &MapFieldWriter{Schema: d.schema} + + // Initialize the reader for getting data from the + // underlying sources (config, diff, etc.) + readers := make(map[string]FieldReader) + if d.raw != nil { + readers["raw"] = &MapFieldReader{ + Schema: d.schema, + Map: BasicMapReader(d.raw), } } - d.reader = &MapFieldReader{ + readers["set"] = &MapFieldReader{ Schema: d.schema, - Map: BasicMapReader(d.writer.Map()), + Map: BasicMapReader(d.setWriter.Map()), + } + d.multiReader = &MultiLevelFieldReader{ + Levels: []string{"raw", "set"}, + Readers: readers, } } @@ -110,7 +114,7 @@ func (d *IdentityData) getRaw(key string) getResult { func (d *IdentityData) get(addr []string) getResult { d.once.Do(d.init) - result, err := d.reader.ReadField(addr) + result, err := d.multiReader.ReadFieldMerge(addr, "set") if err != nil { panic(err) diff --git a/helper/schema/identity_data_test.go b/helper/schema/identity_data_test.go new file mode 100644 index 00000000000..e85ef249f0d --- /dev/null +++ b/helper/schema/identity_data_test.go @@ -0,0 +1,669 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "reflect" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestIdentityDataGet(t *testing.T) { + cases := map[string]struct { + IdentitySchema map[string]*Schema + State *terraform.InstanceState + Diff *terraform.InstanceDiff + Key string + Value interface{} + }{ + "no state, empty diff": { + IdentitySchema: map[string]*Schema{ + "region": { + Type: TypeString, + RequiredForImport: true, + }, + }, + + State: nil, + + Diff: &terraform.InstanceDiff{}, + + Key: "region", + Value: "", + }, + + "no state, diff with identity": { + IdentitySchema: map[string]*Schema{ + "region": { + Type: TypeString, + RequiredForImport: true, + }, + }, + + State: nil, + + Diff: &terraform.InstanceDiff{ + Identity: map[string]string{ + "region": "foo", + }, + }, + + Key: "region", + Value: "foo", + }, + + "state with identity, no diff": { + IdentitySchema: map[string]*Schema{ + "region": { + Type: TypeString, + RequiredForImport: true, + }, + }, + + State: &terraform.InstanceState{ + Identity: map[string]string{ + "region": "bar", + }, + }, + + Diff: nil, + + Key: "region", + + Value: "bar", + }, + + "state with identity, empty diff": { + IdentitySchema: map[string]*Schema{ + "region": { + Type: TypeString, + RequiredForImport: true, + }, + }, + + State: &terraform.InstanceState{ + Identity: map[string]string{ + "region": "foo", + }, + }, + + Diff: &terraform.InstanceDiff{}, + + Key: "region", + Value: "foo", // This is different than for resource data – which would be empty + }, + + "int type: state with identity, no diff": { + IdentitySchema: map[string]*Schema{ + "port": { + Type: TypeInt, + RequiredForImport: true, + }, + }, + + State: &terraform.InstanceState{ + Identity: map[string]string{ + "port": "80", + }, + }, + + Diff: nil, + + Key: "port", + + Value: 80, + }, + + "int list type: state with identity, empty diff": { + IdentitySchema: map[string]*Schema{ + "ports": { + Type: TypeList, + Elem: &Schema{Type: TypeInt}, + RequiredForImport: true, + }, + }, + + State: &terraform.InstanceState{ + Identity: map[string]string{ + "ports.#": "3", + "ports.0": "1", + "ports.1": "2", + "ports.2": "5", + }, + }, + + Key: "ports.1", + + Value: 2, + }, + + "int list type length: state with identity, empty diff": { + IdentitySchema: map[string]*Schema{ + "ports": { + Type: TypeList, + Elem: &Schema{Type: TypeInt}, + }, + }, + + State: &terraform.InstanceState{ + Identity: map[string]string{ + "ports.#": "3", + "ports.0": "1", + "ports.1": "2", + "ports.2": "5", + }, + }, + + Key: "ports.#", + + Value: 3, + }, + + "int list type length: empty state, empty diff": { + IdentitySchema: map[string]*Schema{ + "ports": { + Type: TypeList, + Elem: &Schema{Type: TypeInt}, + RequiredForImport: true, + }, + }, + + State: nil, + + Key: "ports.#", + + Value: 0, + }, + + "int list type all: state with identity, empty diff": { + IdentitySchema: map[string]*Schema{ + "ports": { + Type: TypeList, + Elem: &Schema{Type: TypeInt}, + RequiredForImport: true, + }, + }, + + State: &terraform.InstanceState{ + Identity: map[string]string{ + "ports.#": "3", + "ports.0": "1", + "ports.1": "2", + "ports.2": "5", + }, + }, + + Key: "ports", + + Value: []interface{}{1, 2, 5}, + }, + + "full object: empty state, diff with identity": { + IdentitySchema: map[string]*Schema{ + "region": { + Type: TypeString, + RequiredForImport: true, + }, + }, + + State: nil, + + Diff: &terraform.InstanceDiff{ + Identity: map[string]string{ + "region": "foo", + }, + }, + + Key: "", + + Value: map[string]interface{}{ + "region": "foo", + }, + }, + + "float zero: empty state, empty diff": { + IdentitySchema: map[string]*Schema{ + "ratio": { + Type: TypeFloat, + RequiredForImport: true, + }, + }, + + State: nil, + + Diff: nil, + + Key: "ratio", + + Value: 0.0, + }, + + "float: state with identity, empty diff": { + IdentitySchema: map[string]*Schema{ + "ratio": { + Type: TypeFloat, + RequiredForImport: true, + }, + }, + + State: &terraform.InstanceState{ + Identity: map[string]string{ + "ratio": "0.5", + }, + }, + + Diff: nil, + + Key: "ratio", + + Value: 0.5, + }, + + "float: state with identity, diff with identity": { + IdentitySchema: map[string]*Schema{ + "ratio": { + Type: TypeFloat, + RequiredForImport: true, + }, + }, + + State: &terraform.InstanceState{ + Identity: map[string]string{ + "ratio": "-0.5", + }, + }, + + Diff: &terraform.InstanceDiff{ + Identity: map[string]string{ + "ratio": "33.0", + }, + }, + + Key: "ratio", + + Value: 33.0, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + schema := map[string]*Schema{} + d, err := schemaMapWithIdentity{schema, tc.IdentitySchema}.Data(tc.State, tc.Diff) + if err != nil { + t.Fatalf("err: %s", err) + } + + identity, err := d.Identity() + if err != nil { + t.Fatalf("err: %s", err) + } + + v := identity.Get(tc.Key) + + if !reflect.DeepEqual(v, tc.Value) { + t.Fatalf("Bad: %s\n\n%#v\n\nExpected: %#v", name, v, tc.Value) + } + }) + } +} + +func TestIdentityDataGetOk(t *testing.T) { + cases := []struct { + IdentitySchema map[string]*Schema + State *terraform.InstanceState + Diff *terraform.InstanceDiff + Key string + Value interface{} + Ok bool + }{ + /* + * Primitives + */ + { + IdentitySchema: map[string]*Schema{ + "region": { + Type: TypeString, + RequiredForImport: true, + }, + }, + + State: nil, + + Diff: &terraform.InstanceDiff{ + Identity: map[string]string{ + "region": "", + }, + }, + + Key: "region", + Value: "", + Ok: false, + }, + + { + IdentitySchema: map[string]*Schema{ + "region": { + Type: TypeString, + RequiredForImport: true, + }, + }, + + State: nil, + + Diff: nil, + + Key: "region", + Value: "", + Ok: false, + }, + + /* + * Lists + */ + + { + IdentitySchema: map[string]*Schema{ + "ports": { + Type: TypeList, + Elem: &Schema{Type: TypeInt}, + RequiredForImport: true, + }, + }, + + State: nil, + + Diff: nil, + + Key: "ports", + Value: []interface{}{}, + Ok: false, + }, + } + + for i, tc := range cases { + schema := map[string]*Schema{} + d, err := schemaMapWithIdentity{schema, tc.IdentitySchema}.Data(tc.State, tc.Diff) + if err != nil { + t.Fatalf("err: %s", err) + } + + identity, err := d.Identity() + if err != nil { + t.Fatalf("err: %s", err) + } + + v, ok := identity.GetOk(tc.Key) + if s, ok := v.(*Set); ok { + v = s.List() + } + + if !reflect.DeepEqual(v, tc.Value) { + t.Fatalf("Bad: %d\n\n%#v", i, v) + } + if ok != tc.Ok { + t.Fatalf("%d: expected ok: %t, got: %t", i, tc.Ok, ok) + } + } +} + +func TestIdentityDataSet(t *testing.T) { + var testNilPtr *string + + cases := map[string]struct { + IdentitySchema map[string]*Schema + State *terraform.InstanceState + Diff *terraform.InstanceDiff + Key string + Value interface{} + Err bool + GetKey string + GetValue interface{} + }{ + "basic string": { + IdentitySchema: map[string]*Schema{ + "region": { + Type: TypeString, + RequiredForImport: true, + }, + }, + + State: nil, + + Diff: nil, + + Key: "region", + Value: "foo", + + GetKey: "region", + GetValue: "foo", + }, + + "basic int": { + IdentitySchema: map[string]*Schema{ + "port": { + Type: TypeInt, + }, + }, + + State: nil, + + Diff: nil, + + Key: "port", + Value: 80, + + GetKey: "port", + GetValue: 80, + }, + + "basic bool": { + IdentitySchema: map[string]*Schema{ + "vpc": { + Type: TypeBool, + }, + }, + + State: nil, + + Diff: nil, + + Key: "vpc", + Value: true, + + GetKey: "vpc", + GetValue: true, + }, + + "basic bool false": { + IdentitySchema: map[string]*Schema{ + "vpc": { + Type: TypeBool, + }, + }, + + State: nil, + + Diff: nil, + + Key: "vpc", + Value: false, + + GetKey: "vpc", + GetValue: false, + }, + + "invalid type": { + IdentitySchema: map[string]*Schema{ + "region": { + Type: TypeString, + RequiredForImport: true, + }, + }, + + State: nil, + + Diff: nil, + + Key: "region", + Value: 80, + Err: true, + + GetKey: "region", + GetValue: "", + }, + + "list of primitives - set list": { + IdentitySchema: map[string]*Schema{ + "ports": { + Type: TypeList, + + Elem: &Schema{Type: TypeInt}, + }, + }, + + State: nil, + + Diff: nil, + + Key: "ports", + Value: []int{1, 2, 5}, + + GetKey: "ports", + GetValue: []interface{}{1, 2, 5}, + }, + + "list of primitives - set list with error": { + IdentitySchema: map[string]*Schema{ + "ports": { + Type: TypeList, + + Elem: &Schema{Type: TypeInt}, + }, + }, + + State: nil, + + Diff: nil, + + Key: "ports", + Value: []interface{}{1, "NOPE", 5}, + Err: true, + + GetKey: "ports", + GetValue: []interface{}{}, + }, + + "list of floats - set list": { + IdentitySchema: map[string]*Schema{ + "ratios": { + Type: TypeList, + + Elem: &Schema{Type: TypeFloat}, + }, + }, + + State: nil, + + Diff: nil, + + Key: "ratios", + Value: []float64{1.0, 2.2, 5.5}, + + GetKey: "ratios", + GetValue: []interface{}{1.0, 2.2, 5.5}, + }, + + "basic pointer": { + IdentitySchema: map[string]*Schema{ + "region": { + Type: TypeString, + RequiredForImport: true, + }, + }, + + State: nil, + + Diff: nil, + + Key: "region", + Value: testPtrTo("foo"), + + GetKey: "region", + GetValue: "foo", + }, + + "basic nil value": { + IdentitySchema: map[string]*Schema{ + "region": { + Type: TypeString, + RequiredForImport: true, + }, + }, + + State: nil, + + Diff: nil, + + Key: "region", + Value: testPtrTo(nil), + + GetKey: "region", + GetValue: "", + }, + + "basic nil pointer": { + IdentitySchema: map[string]*Schema{ + "region": { + Type: TypeString, + RequiredForImport: true, + }, + }, + + State: nil, + + Diff: nil, + + Key: "region", + Value: testNilPtr, + + GetKey: "region", + GetValue: "", + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + + schema := map[string]*Schema{} + + d, err := schemaMapWithIdentity{schema, tc.IdentitySchema}.Data(tc.State, tc.Diff) + if err != nil { + t.Fatalf("err: %s", err) + } + + identity, err := d.Identity() + if err != nil { + t.Fatalf("err: %s", err) + } + + err = identity.Set(tc.Key, tc.Value) + if err != nil != tc.Err { + t.Fatalf("%s err: %s", name, err) + } + + // we retrieve a new identity to ensure memoization is working + identity, err = d.Identity() + if err != nil { + t.Fatalf("err: %s", err) + } + + v := identity.Get(tc.GetKey) + + if !reflect.DeepEqual(v, tc.GetValue) { + t.Fatalf("Get Bad: %s\n\n%#v", name, v) + } + }) + } +} diff --git a/helper/schema/resource_data.go b/helper/schema/resource_data.go index 50090f3706e..096f449d1fb 100644 --- a/helper/schema/resource_data.go +++ b/helper/schema/resource_data.go @@ -750,6 +750,9 @@ func (d *ResourceData) Identity() (*IdentityData, error) { if d.state != nil && d.state.Identity != nil { identityData = d.state.Identity } + if d.diff != nil && d.diff.Identity != nil { + identityData = d.diff.Identity + } d.newIdentity = &IdentityData{ schema: d.identitySchema, From 666604091327bf8ea7b4558955f08f1682ecce96 Mon Sep 17 00:00:00 2001 From: Ansgar Mertens Date: Wed, 26 Mar 2025 20:41:02 +0100 Subject: [PATCH 24/53] Add tests for ResourceData#Identity method (#1458) --- helper/schema/resource_data_test.go | 136 ++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/helper/schema/resource_data_test.go b/helper/schema/resource_data_test.go index 5e1d53e8a01..e81ca198c8e 100644 --- a/helper/schema/resource_data_test.go +++ b/helper/schema/resource_data_test.go @@ -4178,6 +4178,142 @@ func TestResourceDataSetType(t *testing.T) { } } +func TestResourceDataIdentity(t *testing.T) { + d := &ResourceData{ + identitySchema: map[string]*Schema{ + "foo": { + Type: TypeString, + RequiredForImport: true, + }, + }, + } + d.SetId("baz") // just required to be able to call .State() + identity, err := d.Identity() + if err != nil { + t.Fatalf("err: %s", err) + } + + // test setting + err = identity.Set("foo", "bar") + if err != nil { + t.Fatalf("err: %s", err) + } + + // test memoization + identity2, err := d.Identity() + if err != nil { + t.Fatalf("err: %s", err) + } + if identity2.Get("foo").(string) != "bar" { + t.Fatalf("expected identity to contain value for foo: %#v", identity2) + } + + // test identity added to state + state := d.State() + if state.Identity == nil { + t.Fatalf("expected identity to be added to state: %#v", state) + } + if state.Identity["foo"] != "bar" { + t.Fatalf("expected identity to contain value for foo: %#v", state) + } +} + +func TestResourceDataIdentity_initial_data_from_state(t *testing.T) { + d := &ResourceData{ + identitySchema: map[string]*Schema{ + "foo": { + Type: TypeString, + RequiredForImport: true, + }, + }, + state: &terraform.InstanceState{ + Identity: map[string]string{ + "foo": "bar", + }, + }, + } + identity, err := d.Identity() + if err != nil { + t.Fatalf("err: %s", err) + } + if identity.Get("foo").(string) != "bar" { + t.Fatalf("expected identity to contain value for foo: %#v", identity) + } +} + +func TestResourceDataIdentity_initial_data_from_diff(t *testing.T) { + d := &ResourceData{ + identitySchema: map[string]*Schema{ + "foo": { + Type: TypeString, + RequiredForImport: true, + }, + }, + // we also keep this to ensure diff takes precedence over state + state: &terraform.InstanceState{ + Identity: map[string]string{ + "foo": "bar", + }, + }, + diff: &terraform.InstanceDiff{ + Identity: map[string]string{ + "foo": "baz", + }, + }, + } + identity, err := d.Identity() + if err != nil { + t.Fatalf("err: %s", err) + } + if identity.Get("foo").(string) != "baz" { + t.Fatalf("expected identity to contain baz value for foo: %#v", identity) + } +} + +func TestResourceDataIdentity_changing_initial_data(t *testing.T) { + d := &ResourceData{ + identitySchema: map[string]*Schema{ + "foo": { + Type: TypeString, + RequiredForImport: true, + }, + }, + diff: &terraform.InstanceDiff{ + Identity: map[string]string{ + "foo": "baz", + }, + }, + } + d.SetId("bar") // just required to be able to call .State() + identity, err := d.Identity() + if err != nil { + t.Fatalf("err: %s", err) + } + err = identity.Set("foo", "qux") + if err != nil { + t.Fatalf("err: %s", err) + } + + state := d.State() + if state.Identity == nil { + t.Fatalf("expected identity to be added to state: %#v", state) + } + if state.Identity["foo"] != "qux" { + t.Fatalf("expected identity to contain qux value for foo: %#v", state) + } +} + +func TestResourceDataIdentity_no_schema(t *testing.T) { + d := &ResourceData{} + _, err := d.Identity() + if err == nil { + t.Fatalf("expected error since there's no identity schema, got: nil") + } + if diff := cmp.Diff("Resource does not have Identity schema. Please set one in order to use Identity(). This is always a problem in the provider code.", err.Error()); diff != "" { + t.Fatalf("unexpected error message (-want +got):\n%s", diff) + } +} + func testPtrTo(raw interface{}) interface{} { return &raw } From dee0dd6227357359d86687a91bbbc025e3427268 Mon Sep 17 00:00:00 2001 From: Ansgar Mertens Date: Thu, 27 Mar 2025 15:33:21 +0100 Subject: [PATCH 25/53] TFECO-9166: Add support for ResourceIdentity#SchemaFunc to allow providers to save RAM (#1459) * Add support for ResourceIdentity#SchemaFunc to allow providers to save RAM * handle nil ResourceIdentity for SchemaMap() and simplify code --- helper/schema/core_schema.go | 4 +-- helper/schema/grpc_provider_test.go | 40 +++++++++++++------------ helper/schema/resource.go | 30 +++++-------------- helper/schema/resource_identity.go | 21 +++++++++++++ helper/schema/resource_identity_test.go | 13 ++++++++ helper/schema/shims.go | 6 +--- 6 files changed, 65 insertions(+), 49 deletions(-) create mode 100644 helper/schema/resource_identity_test.go diff --git a/helper/schema/core_schema.go b/helper/schema/core_schema.go index 388aafd1c12..79f78a1dc31 100644 --- a/helper/schema/core_schema.go +++ b/helper/schema/core_schema.go @@ -389,11 +389,11 @@ func (r *Resource) CoreIdentitySchema() (*configschema.Block, error) { } func (r *Resource) coreIdentitySchema() (*configschema.Block, error) { - if r.Identity == nil || r.Identity.Schema == nil { + if r.Identity.SchemaMap() == nil { return nil, fmt.Errorf("resource does not have an identity schema") } // while there is schemaMapWithIdentity, we don't need to use it here // as we're only interested in the existing CoreConfigSchema() method // to convert our schema - return schemaMap(r.Identity.Schema).CoreConfigSchema(), nil + return schemaMap(r.Identity.SchemaMap()).CoreConfigSchema(), nil } diff --git a/helper/schema/grpc_provider_test.go b/helper/schema/grpc_provider_test.go index 95e70999062..f760760f09b 100644 --- a/helper/schema/grpc_provider_test.go +++ b/helper/schema/grpc_provider_test.go @@ -3319,25 +3319,27 @@ func TestGRPCProviderServerGetResourceIdentitySchemas(t *testing.T) { }, "test_resource2": { Identity: &ResourceIdentity{ - Schema: map[string]*Schema{ - "test2": { - Type: TypeString, - RequiredForImport: false, - OptionalForImport: true, - Description: "test resource 2", - }, - "test2-2": { - Type: TypeList, - RequiredForImport: false, - OptionalForImport: true, - Description: "test resource 2-2", - }, - "test2-3": { - Type: TypeInt, - RequiredForImport: false, - OptionalForImport: true, - Description: "test resource 2-3", - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "test2": { + Type: TypeString, + RequiredForImport: false, + OptionalForImport: true, + Description: "test resource 2", + }, + "test2-2": { + Type: TypeList, + RequiredForImport: false, + OptionalForImport: true, + Description: "test resource 2-2", + }, + "test2-3": { + Type: TypeInt, + RequiredForImport: false, + OptionalForImport: true, + Description: "test resource 2-3", + }, + } }, }, }, diff --git a/helper/schema/resource.go b/helper/schema/resource.go index ac6db21386b..077a6ddb09b 100644 --- a/helper/schema/resource.go +++ b/helper/schema/resource.go @@ -728,11 +728,7 @@ func (r *Resource) ShimInstanceStateFromValue(state cty.Value) (*terraform.Insta // We now rebuild the state through the ResourceData, so that the set indexes // match what helper/schema expects. - var identity map[string]*Schema - if r.Identity != nil { - identity = r.Identity.Schema - } - data, err := schemaMapWithIdentity{r.SchemaMap(), identity}.Data(s, nil) + data, err := schemaMapWithIdentity{r.SchemaMap(), r.Identity.SchemaMap()}.Data(s, nil) if err != nil { return nil, err } @@ -905,11 +901,7 @@ func (r *Resource) Apply( s *terraform.InstanceState, d *terraform.InstanceDiff, meta interface{}) (*terraform.InstanceState, diag.Diagnostics) { - var identity map[string]*Schema - if r.Identity != nil { - identity = r.Identity.Schema - } - schema := schemaMapWithIdentity{r.SchemaMap(), identity} + schema := schemaMapWithIdentity{r.SchemaMap(), r.Identity.SchemaMap()} data, err := schema.Data(s, d) if err != nil { return s, diag.FromErr(err) @@ -1033,12 +1025,8 @@ func (r *Resource) SimpleDiff( c *terraform.ResourceConfig, meta interface{}) (*terraform.InstanceDiff, error) { - var identity map[string]*Schema - if r.Identity != nil { - identity = r.Identity.Schema - } // TODO: figure out if it makes sense to be able to set identity in CustomizeDiff at all - instanceDiff, err := schemaMapWithIdentity{r.SchemaMap(), identity}.Diff(ctx, s, c, r.CustomizeDiff, meta, false) + instanceDiff, err := schemaMapWithIdentity{r.SchemaMap(), r.Identity.SchemaMap()}.Diff(ctx, s, c, r.CustomizeDiff, meta, false) if err != nil { return instanceDiff, err } @@ -1127,11 +1115,7 @@ func (r *Resource) RefreshWithoutUpgrade( } } - var identity map[string]*Schema - if r.Identity != nil { - identity = r.Identity.Schema - } - schema := schemaMapWithIdentity{r.SchemaMap(), identity} + schema := schemaMapWithIdentity{r.SchemaMap(), r.Identity.SchemaMap()} if r.Exists != nil { // Make a copy of data so that if it is modified it doesn't @@ -1442,7 +1426,7 @@ func (r *Resource) Data(s *terraform.InstanceState) *ResourceData { func (r *Resource) TestResourceData() *ResourceData { return &ResourceData{ schema: r.SchemaMap(), - identitySchema: r.Identity.Schema, + identitySchema: r.Identity.SchemaMap(), } } @@ -1489,11 +1473,11 @@ func (r *ResourceIdentity) InternalIdentityValidate() error { return fmt.Errorf(`The resource identity is empty`) } - if len(r.Schema) == 0 { + if len(r.SchemaMap()) == 0 { return fmt.Errorf(`The resource identity schema is empty`) } - for k, v := range r.Schema { + for k, v := range r.SchemaMap() { if !v.OptionalForImport && !v.RequiredForImport { return fmt.Errorf(`OptionalForImport or RequiredForImport must be set for resource identity`) } diff --git a/helper/schema/resource_identity.go b/helper/schema/resource_identity.go index a5f24c191ee..9527ccc2e71 100644 --- a/helper/schema/resource_identity.go +++ b/helper/schema/resource_identity.go @@ -37,6 +37,12 @@ type ResourceIdentity struct { // previous resource schemas. Schema map[string]*Schema + // SchemaFunc is an optional function that returns the schema for the + // identity. Use this field instead of Schema on resource identity + // declarations to prevent storing all identity schema information in + // memory for the lifecycle of a provider. + SchemaFunc func() map[string]*Schema + // New struct, will be similar to (Resource).StateUpgraders IdentityUpgraders []IdentityUpgrader } @@ -70,3 +76,18 @@ type ResourceIdentity struct { // identity schema version data for a managed resource instance. Values must // align to the typing mentioned above. type ResourceIdentityUpgradeFunc func(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) + +// SchemaMap returns the schema information for this Resource whether it is +// defined via the SchemaFunc field or Schema field. The SchemaFunc field, if +// defined, takes precedence over the Schema field. +func (ri *ResourceIdentity) SchemaMap() map[string]*Schema { + if ri == nil { + return nil + } + + if ri.SchemaFunc != nil { + return ri.SchemaFunc() + } + + return ri.Schema +} diff --git a/helper/schema/resource_identity_test.go b/helper/schema/resource_identity_test.go new file mode 100644 index 00000000000..360093af710 --- /dev/null +++ b/helper/schema/resource_identity_test.go @@ -0,0 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import "testing" + +func TestResourceIdentity_SchemaMap_handles_nil_identity(t *testing.T) { + var ri *ResourceIdentity + if ri.SchemaMap() != nil { + t.Fatal("expected nil schema map") + } +} diff --git a/helper/schema/shims.go b/helper/schema/shims.go index 9b8159fd297..fb351358df4 100644 --- a/helper/schema/shims.go +++ b/helper/schema/shims.go @@ -43,11 +43,7 @@ func diffFromValues(ctx context.Context, prior, planned, config cty.Value, res * removeConfigUnknowns(cfg.Config) removeConfigUnknowns(cfg.Raw) - var identity map[string]*Schema - if res.Identity != nil { - identity = res.Identity.Schema - } - diff, err := schemaMapWithIdentity{res.SchemaMap(), identity}.Diff(ctx, instanceState, cfg, cust, nil, false) + diff, err := schemaMapWithIdentity{res.SchemaMap(), res.Identity.SchemaMap()}.Diff(ctx, instanceState, cfg, cust, nil, false) if err != nil { return nil, err } From 4186bf4f00b06b8a095bd2e65a9fda0ffec6f98b Mon Sep 17 00:00:00 2001 From: Ansgar Mertens Date: Mon, 31 Mar 2025 14:43:59 +0200 Subject: [PATCH 26/53] Remove Schema attribute for identity in favor of SchemaFunc (#1461) * Remove Schema attribute for identity in favor of SchemaFunc * fix: don't call function if it's nil, return nil instead --- helper/schema/grpc_provider_test.go | 156 +++++++------ helper/schema/resource_identity.go | 32 ++- helper/schema/resource_test.go | 346 ++++++++++++++++------------ 3 files changed, 306 insertions(+), 228 deletions(-) diff --git a/helper/schema/grpc_provider_test.go b/helper/schema/grpc_provider_test.go index f760760f09b..600a4ade34e 100644 --- a/helper/schema/grpc_provider_test.go +++ b/helper/schema/grpc_provider_test.go @@ -3307,13 +3307,15 @@ func TestGRPCProviderServerGetResourceIdentitySchemas(t *testing.T) { "test_resource1": { Identity: &ResourceIdentity{ Version: 1, - Schema: map[string]*Schema{ - "test": { - Type: TypeString, - RequiredForImport: true, - OptionalForImport: false, - Description: "test resource", - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "test": { + Type: TypeString, + RequiredForImport: true, + OptionalForImport: false, + Description: "test resource", + }, + } }, }, }, @@ -3392,15 +3394,17 @@ func TestGRPCProviderServerGetResourceIdentitySchemas(t *testing.T) { ResourcesMap: map[string]*Resource{ "test_resource": { Identity: &ResourceIdentity{ - Schema: map[string]*Schema{ - "bool_attr": {Type: TypeBool, Description: "Boolean attribute"}, - "float_attr": {Type: TypeFloat, Description: "Float attribute"}, - "int_attr": {Type: TypeInt, Description: "Int attribute"}, - "list_bool_attr": {Type: TypeList, Elem: TypeBool, Description: "List Bool attribute"}, - "list_float_attr": {Type: TypeList, Elem: TypeFloat, Description: "List Float attribute"}, - "list_int_attr": {Type: TypeList, Elem: TypeInt, Description: "List Int attribute"}, - "list_str_attr": {Type: TypeList, Elem: TypeString, Description: "List String attribute"}, - "string_attr": {Type: TypeString, Description: "String attribute"}, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "bool_attr": {Type: TypeBool, Description: "Boolean attribute"}, + "float_attr": {Type: TypeFloat, Description: "Float attribute"}, + "int_attr": {Type: TypeInt, Description: "Int attribute"}, + "list_bool_attr": {Type: TypeList, Elem: TypeBool, Description: "List Bool attribute"}, + "list_float_attr": {Type: TypeList, Elem: TypeFloat, Description: "List Float attribute"}, + "list_int_attr": {Type: TypeList, Elem: TypeInt, Description: "List Int attribute"}, + "list_str_attr": {Type: TypeList, Elem: TypeString, Description: "List String attribute"}, + "string_attr": {Type: TypeString, Description: "String attribute"}, + } }, }, }, @@ -3449,7 +3453,9 @@ func TestGRPCProviderServerGetResourceIdentitySchemas(t *testing.T) { "test_resource1": { Identity: &ResourceIdentity{ Version: 1, - Schema: map[string]*Schema{}, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{} + }, }, }, }, @@ -3501,13 +3507,15 @@ func TestUpgradeResourceIdentity_jsonState(t *testing.T) { SchemaVersion: 1, Identity: &ResourceIdentity{ Version: 1, - Schema: map[string]*Schema{ - "id": { - Type: TypeString, - RequiredForImport: true, - OptionalForImport: false, - Description: "id of thing", - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "id": { + Type: TypeString, + RequiredForImport: true, + OptionalForImport: false, + Description: "id of thing", + }, + } }, IdentityUpgraders: []IdentityUpgrader{ { @@ -3584,13 +3592,15 @@ func TestUpgradeResourceIdentity_removedAttr(t *testing.T) { SchemaVersion: 1, Identity: &ResourceIdentity{ Version: 1, - Schema: map[string]*Schema{ - "id": { - Type: TypeString, - RequiredForImport: true, - OptionalForImport: false, - Description: "id of thing", - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "id": { + Type: TypeString, + RequiredForImport: true, + OptionalForImport: false, + Description: "id of thing", + }, + } }, IdentityUpgraders: []IdentityUpgrader{ { @@ -3669,13 +3679,15 @@ func TestUpgradeResourceIdentity_jsonStateBigInt(t *testing.T) { SchemaVersion: 1, Identity: &ResourceIdentity{ Version: 1, - Schema: map[string]*Schema{ - "int": { - Type: TypeInt, - RequiredForImport: true, - OptionalForImport: false, - Description: "", - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "int": { + Type: TypeInt, + RequiredForImport: true, + OptionalForImport: false, + Description: "", + }, + } }, }, } @@ -5066,15 +5078,17 @@ func TestReadResource(t *testing.T) { }, Identity: &ResourceIdentity{ Version: 1, - Schema: map[string]*Schema{ - "instance_id": { - Type: TypeString, - RequiredForImport: true, - }, - "region": { - Type: TypeString, - OptionalForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "instance_id": { + Type: TypeString, + RequiredForImport: true, + }, + "region": { + Type: TypeString, + OptionalForImport: true, + }, + } }, }, ReadContext: func(ctx context.Context, d *ResourceData, meta interface{}) diag.Diagnostics { @@ -5226,7 +5240,9 @@ func TestReadResource(t *testing.T) { SchemaVersion: 1, Identity: &ResourceIdentity{ Version: 1, - Schema: map[string]*Schema{}, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{} + }, }, }, }, @@ -5538,11 +5554,13 @@ func TestPlanResourceChange(t *testing.T) { }, Identity: &ResourceIdentity{ Version: 1, - Schema: map[string]*Schema{ - "name": { - Type: TypeString, - RequiredForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "name": { + Type: TypeString, + RequiredForImport: true, + }, + } }, }, }, @@ -5644,11 +5662,13 @@ func TestPlanResourceChange(t *testing.T) { }, Identity: &ResourceIdentity{ Version: 1, - Schema: map[string]*Schema{ - "name": { - Type: TypeString, - RequiredForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "name": { + Type: TypeString, + RequiredForImport: true, + }, + } }, }, CustomizeDiff: func(ctx context.Context, d *ResourceDiff, meta interface{}) error { @@ -5827,7 +5847,9 @@ func TestPlanResourceChange(t *testing.T) { }, Identity: &ResourceIdentity{ Version: 1, - Schema: map[string]*Schema{}, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{} + }, }, }, }, @@ -6833,11 +6855,13 @@ func TestApplyResourceChange(t *testing.T) { Schema: map[string]*Schema{}, Identity: &ResourceIdentity{ Version: 1, - Schema: map[string]*Schema{ - "ident": { - Type: TypeString, - RequiredForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "ident": { + Type: TypeString, + RequiredForImport: true, + }, + } }, }, }, @@ -6988,7 +7012,9 @@ func TestApplyResourceChange(t *testing.T) { Schema: map[string]*Schema{}, Identity: &ResourceIdentity{ Version: 1, - Schema: map[string]*Schema{}, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{} + }, }, }, }, diff --git a/helper/schema/resource_identity.go b/helper/schema/resource_identity.go index 9527ccc2e71..5609416c448 100644 --- a/helper/schema/resource_identity.go +++ b/helper/schema/resource_identity.go @@ -32,15 +32,16 @@ type ResourceIdentity struct { // Version is the identity schema version. Version int64 - // Schema is the structure and type information for the identity. - // The types allowed in this Schema will be more restricted than - // previous resource schemas. - Schema map[string]*Schema - - // SchemaFunc is an optional function that returns the schema for the - // identity. Use this field instead of Schema on resource identity - // declarations to prevent storing all identity schema information in - // memory for the lifecycle of a provider. + // SchemaFunc is the function that returns the schema for the + // identity. Using a function for this field allows to prevent + // storing all identity schema information in memory for the + // lifecycle of a provider. + // The types of the schema values are restricted to the types: + // - TypeBool + // - TypeFloat + // - TypeInt + // - TypeString + // - TypeList (of any of the above types) SchemaFunc func() map[string]*Schema // New struct, will be similar to (Resource).StateUpgraders @@ -77,17 +78,12 @@ type ResourceIdentity struct { // align to the typing mentioned above. type ResourceIdentityUpgradeFunc func(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) -// SchemaMap returns the schema information for this Resource whether it is -// defined via the SchemaFunc field or Schema field. The SchemaFunc field, if -// defined, takes precedence over the Schema field. +// SchemaMap returns the schema information for this resource identity +// defined via the SchemaFunc field. func (ri *ResourceIdentity) SchemaMap() map[string]*Schema { - if ri == nil { + if ri == nil || ri.SchemaFunc == nil { return nil } - if ri.SchemaFunc != nil { - return ri.SchemaFunc() - } - - return ri.Schema + return ri.SchemaFunc() } diff --git a/helper/schema/resource_test.go b/helper/schema/resource_test.go index cf64a6e6045..3a9c03f48ff 100644 --- a/helper/schema/resource_test.go +++ b/helper/schema/resource_test.go @@ -1677,12 +1677,14 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "OptionalForImport and RequiredForImport both false": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - OptionalForImport: false, - RequiredForImport: false, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeInt, + OptionalForImport: false, + RequiredForImport: false, + }, + } }, }, true, @@ -1690,12 +1692,14 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "OptionalForImport and RequiredForImport both true": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - OptionalForImport: true, - RequiredForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeInt, + OptionalForImport: true, + RequiredForImport: true, + }, + } }, }, true, @@ -1703,8 +1707,10 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "TypeMap is not valid": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": {Type: TypeMap, OptionalForImport: true}, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": {Type: TypeMap, OptionalForImport: true}, + } }, }, true, @@ -1712,8 +1718,10 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "TypeSet is not valid": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": {Type: TypeSet, OptionalForImport: true}, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": {Type: TypeSet, OptionalForImport: true}, + } }, }, true, @@ -1721,8 +1729,10 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "TypeObject is not valid": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": {Type: typeObject, OptionalForImport: true}, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": {Type: typeObject, OptionalForImport: true}, + } }, }, true, @@ -1730,8 +1740,10 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "TypeInvalid is not valid": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": {Type: TypeInvalid, OptionalForImport: true}, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": {Type: TypeInvalid, OptionalForImport: true}, + } }, }, true, @@ -1739,10 +1751,12 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "TypeList contains TypeMap": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeList, Elem: TypeMap, OptionalForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeList, Elem: TypeMap, OptionalForImport: true, + }, + } }, }, true, @@ -1750,18 +1764,20 @@ func TestResourceInternalIdentityValidate(t *testing.T) { " TypeList contains TypeSet": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeList, - Elem: &Resource{ - Schema: map[string]*Schema{ - "bar": { - Type: TypeSet, - RequiredForImport: true, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeList, + Elem: &Resource{ + Schema: map[string]*Schema{ + "bar": { + Type: TypeSet, + RequiredForImport: true, + }, }, }, }, - }, + } }, }, true, @@ -1769,10 +1785,12 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "TypeList contains TypeInvalid": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeList, Elem: TypeInvalid, OptionalForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeList, Elem: TypeInvalid, OptionalForImport: true, + }, + } }, }, true, @@ -1780,12 +1798,14 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "ForceNew is set": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - ForceNew: true, - OptionalForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeInt, + ForceNew: true, + OptionalForImport: true, + }, + } }, }, true, @@ -1793,12 +1813,14 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "Optional is set": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - Optional: true, - OptionalForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeInt, + Optional: true, + OptionalForImport: true, + }, + } }, }, true, @@ -1806,12 +1828,14 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "Required is set": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - Required: true, - OptionalForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeInt, + Required: true, + OptionalForImport: true, + }, + } }, }, true, @@ -1819,12 +1843,14 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "WriteOnly is set": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - WriteOnly: true, - OptionalForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeInt, + WriteOnly: true, + OptionalForImport: true, + }, + } }, }, true, @@ -1832,12 +1858,14 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "Computed is set": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - Computed: true, - OptionalForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeInt, + Computed: true, + OptionalForImport: true, + }, + } }, }, true, @@ -1845,12 +1873,14 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "Deprecated is set": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - Deprecated: "deprecated", - OptionalForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeInt, + Deprecated: "deprecated", + OptionalForImport: true, + }, + } }, }, true, @@ -1858,12 +1888,14 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "Default is set": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - Default: 42, - OptionalForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeInt, + Default: 42, + OptionalForImport: true, + }, + } }, }, true, @@ -1871,12 +1903,14 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "MaxItems is set": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - MaxItems: 5, - OptionalForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeInt, + MaxItems: 5, + OptionalForImport: true, + }, + } }, }, true, @@ -1884,12 +1918,14 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "MinItems is set": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - MinItems: 1, - OptionalForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeInt, + MinItems: 1, + OptionalForImport: true, + }, + } }, }, true, @@ -1897,12 +1933,14 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "DiffSuppressOnRefresh is set": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - DiffSuppressOnRefresh: true, - OptionalForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeInt, + DiffSuppressOnRefresh: true, + OptionalForImport: true, + }, + } }, }, true, @@ -1910,12 +1948,14 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "RequiredWith is set": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - RequiredWith: []string{"bar"}, - OptionalForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeInt, + RequiredWith: []string{"bar"}, + OptionalForImport: true, + }, + } }, }, true, @@ -1923,12 +1963,14 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "ComputedWhen is set": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - ComputedWhen: []string{"bar"}, - OptionalForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeInt, + ComputedWhen: []string{"bar"}, + OptionalForImport: true, + }, + } }, }, true, @@ -1936,12 +1978,14 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "DefaultFunc is set": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - DefaultFunc: func() (interface{}, error) { return 42, nil }, - OptionalForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeInt, + DefaultFunc: func() (interface{}, error) { return 42, nil }, + OptionalForImport: true, + }, + } }, }, true, @@ -1949,12 +1993,14 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "StateFunc is set": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - StateFunc: func(val interface{}) string { return "" }, - OptionalForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeInt, + StateFunc: func(val interface{}) string { return "" }, + OptionalForImport: true, + }, + } }, }, true, @@ -1962,12 +2008,14 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "ValidateFunc is set": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - ValidateFunc: func(val interface{}, key string) (ws []string, es []error) { return nil, nil }, - OptionalForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeInt, + ValidateFunc: func(val interface{}, key string) (ws []string, es []error) { return nil, nil }, + OptionalForImport: true, + }, + } }, }, true, @@ -1975,12 +2023,14 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "AtLeastOneOf is set": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - AtLeastOneOf: []string{"bar"}, - OptionalForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeInt, + AtLeastOneOf: []string{"bar"}, + OptionalForImport: true, + }, + } }, }, true, @@ -1988,12 +2038,14 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "ConflictsWith is set": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - ConflictsWith: []string{"bar"}, - OptionalForImport: true, - }, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeInt, + ConflictsWith: []string{"bar"}, + OptionalForImport: true, + }, + } }, }, true, @@ -2001,9 +2053,11 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "Valid resource identity OptionalForImport": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, OptionalForImport: true}, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeInt, OptionalForImport: true}, + } }, }, false, @@ -2011,9 +2065,11 @@ func TestResourceInternalIdentityValidate(t *testing.T) { "Valid resource identity RequiredorImport": { &ResourceIdentity{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, RequiredForImport: true}, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "foo": { + Type: TypeInt, RequiredForImport: true}, + } }, }, false, From 0f3d65521055ffd6061935f7301eb7d3aa6dd44c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 12:18:37 -0400 Subject: [PATCH 27/53] build(deps): bump golang.org/x/crypto from 0.36.0 to 0.37.0 (#1466) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.36.0 to 0.37.0. - [Commits](https://github.com/golang/crypto/compare/v0.36.0...v0.37.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.37.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 22 ++++++++++------------ 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index c6b9ee7f03d..72e217a90cd 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/reflectwalk v1.0.2 github.com/zclconf/go-cty v1.16.2 - golang.org/x/crypto v0.36.0 + golang.org/x/crypto v0.37.0 ) require ( @@ -50,9 +50,9 @@ require ( github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.37.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect diff --git a/go.sum b/go.sum index 70912916d6d..43dd1f5671d 100644 --- a/go.sum +++ b/go.sum @@ -80,8 +80,6 @@ github.com/hashicorp/terraform-exec v0.22.0 h1:G5+4Sz6jYZfRYUCg6eQgDsqTzkNXV+fP8 github.com/hashicorp/terraform-exec v0.22.0/go.mod h1:bjVbsncaeh8jVdhttWYZuBGj21FcYw6Ia/XfHcNO7lQ= github.com/hashicorp/terraform-json v0.24.0 h1:rUiyF+x1kYawXeRth6fKFm/MdfBS6+lW4NbeATsYz8Q= github.com/hashicorp/terraform-json v0.24.0/go.mod h1:Nfj5ubo9xbu9uiAoZVBsNOjvNKB66Oyrvtit74kC7ow= -github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250313132633-b799d5c93127 h1:V7RFaz+LWQY/S8tiWLuVFQMigEoZOpCF2vv7FW83vVw= -github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250313132633-b799d5c93127/go.mod h1:MfDwS/KnIy2QzCwdRtuqIjZ23gpYa9Vm+Z8cFpx8qtU= github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1 h1:/IZFNUEafGnJGXRe2iNQQ+vtzEw/5qiD+gOxkFrNbi4= github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1/go.mod h1:Tf2HngbyKvovAlGXgBOVGm3EDvbNaN/StUaTXwrej4o= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= @@ -164,8 +162,8 @@ go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= @@ -178,8 +176,8 @@ golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -192,18 +190,18 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= From 3cfe900da5619e89a9b578dbc6e490198e9975ee Mon Sep 17 00:00:00 2001 From: "hashicorp-tsccr[bot]" <129506189+hashicorp-tsccr[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 10:47:38 -0500 Subject: [PATCH 28/53] SEC-090: Automated trusted workflow pinning (2025-04-07) (#1465) * Result of tsccr-helper -log-level=info gha update -latest .github/ * golangci-lint migrate --------- Co-authored-by: hashicorp-tsccr[bot] Co-authored-by: Baraa Basata --- .github/workflows/ci-github-actions.yml | 2 +- .github/workflows/ci-go.yml | 10 ++-- .github/workflows/ci-goreleaser.yml | 4 +- .github/workflows/release.yml | 4 +- .golangci.yml | 65 +++++++++++++++++-------- 5 files changed, 56 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci-github-actions.yml b/.github/workflows/ci-github-actions.yml index 8451dde358b..0773b8bc95b 100644 --- a/.github/workflows/ci-github-actions.yml +++ b/.github/workflows/ci-github-actions.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version-file: 'go.mod' - run: go install github.com/rhysd/actionlint/cmd/actionlint@latest diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index 0cec866afaa..4746969cbaa 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -17,11 +17,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version-file: 'go.mod' - run: go mod download - - uses: golangci/golangci-lint-action@4696ba8babb6127d732c3c6dde519db15edab9ea # v6.5.1 + - uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0 with: version: latest terraform-provider-corner-tfprotov5: @@ -36,7 +36,7 @@ jobs: with: path: terraform-provider-corner repository: hashicorp/terraform-provider-corner - - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version-file: 'go.mod' - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 @@ -60,13 +60,13 @@ jobs: go-version: [ '1.24', '1.23' ] steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version: ${{ matrix.go-version }} - run: go mod download - run: go test -coverprofile=coverage.out ./... - run: go tool cover -html=coverage.out -o coverage.html - - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: go-${{ matrix.go-version }}-coverage path: coverage.html diff --git a/.github/workflows/ci-goreleaser.yml b/.github/workflows/ci-goreleaser.yml index 76735ffb5a2..59a7b3d8f30 100644 --- a/.github/workflows/ci-goreleaser.yml +++ b/.github/workflows/ci-goreleaser.yml @@ -15,9 +15,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version-file: 'go.mod' - - uses: goreleaser/goreleaser-action@90a3faa9d0182683851fbfa97ca1a2cb983bfca3 # v6.2.1 + - uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 with: args: check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 14ee4fc95e1..7860f8587ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -134,7 +134,7 @@ jobs: ref: ${{ inputs.versionNumber }} fetch-depth: 0 - - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version-file: 'go.mod' @@ -143,7 +143,7 @@ jobs: cd .changes sed -e "1{/# /d;}" -e "2{/^$/d;}" ${{ needs.changelog-version.outputs.version }}.md > /tmp/release-notes.txt - - uses: goreleaser/goreleaser-action@90a3faa9d0182683851fbfa97ca1a2cb983bfca3 # v6.2.1 + - uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.golangci.yml b/.golangci.yml index d3d2390bf27..0f296f08a24 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,31 +1,58 @@ -issues: - exclude-rules: - - linters: - - staticcheck - text: 'SA1019: schema.SchemaValidateFunc is deprecated' - max-issues-per-linter: 0 - max-same-issues: 0 - +version: "2" linters: - disable-all: true + default: none enable: + - copyloopvar - durationcheck - errcheck - - copyloopvar - - gofmt - - gosimple + - govet - ineffassign - makezero - nilerr - # - paralleltest # Reference: https://github.com/kunwardeep/paralleltest/issues/14 - predeclared - staticcheck - - usetesting - unconvert - unparam - unused - - govet - -run: - # Prevent false positive timeouts in CI - timeout: 5m \ No newline at end of file + - usetesting + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - staticcheck + text: 'SA1019: schema.SchemaValidateFunc is deprecated' + paths: + - third_party$ + - builtin$ + - examples$ + settings: + staticcheck: + checks: + - all + - '-QF1001' # "could apply De Morgan's law" -- https://staticcheck.dev/docs/checks/#QF1001 + - '-QF1002' # "could use tagged switch" -- https://staticcheck.dev/docs/checks/#QF1002 + - '-QF1004' # "could use strings.ReplaceAll instead" -- https://staticcheck.dev/docs/checks/#QF1004 + - '-QF1007' # "could merge conditional assignment into variable declaration" -- https://staticcheck.dev/docs/checks/#QF1007 + - '-QF1008' # "could remove embedded field "Block" from selector" -- https://staticcheck.dev/docs/checks/#QF1008 + - '-QF1011' # "could omit type *terraform.InstanceState from declaration" -- https://staticcheck.dev/docs/checks/#QF1011 + - '-ST1003' # example: "const autoTFVarsJson should be autoTFVarsJSON" -- https://staticcheck.dev/docs/checks/#ST1003 + - '-ST1005' # "error strings should not end with punctuation or newlines" -- https://staticcheck.dev/docs/checks/#ST1005 + - '-ST1016' # example: "methods on the same type should have the same receiver name (seen 2x "r", 2x "s")" -- https://staticcheck.dev/docs/checks/#ST1016 + - '-ST1023' # example: "should omit type *terraform.InstanceState from declaration;" -- https://staticcheck.dev/docs/checks/#ST1023 +issues: + max-issues-per-linter: 0 + max-same-issues: 0 +formatters: + enable: + - gofmt + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ From 0f3efa19e933f824c5f055a90d361696366a5f90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Apr 2025 09:58:58 -0400 Subject: [PATCH 29/53] build(deps): bump github.com/hashicorp/hc-install from 0.9.1 to 0.9.2 (#1468) Bumps [github.com/hashicorp/hc-install](https://github.com/hashicorp/hc-install) from 0.9.1 to 0.9.2. - [Release notes](https://github.com/hashicorp/hc-install/releases) - [Commits](https://github.com/hashicorp/hc-install/compare/v0.9.1...v0.9.2) --- updated-dependencies: - dependency-name: github.com/hashicorp/hc-install dependency-version: 0.9.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 44 ++++++++++++++++++++++---------------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/go.mod b/go.mod index 72e217a90cd..75c6c81d163 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/hashicorp/go-plugin v1.6.3 github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-version v1.7.0 - github.com/hashicorp/hc-install v0.9.1 + github.com/hashicorp/hc-install v0.9.2 github.com/hashicorp/hcl/v2 v2.23.0 github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-exec v0.22.0 @@ -27,10 +27,10 @@ require ( ) require ( - github.com/ProtonMail/go-crypto v1.1.3 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/agext/levenshtein v1.2.2 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect - github.com/cloudflare/circl v1.3.7 // indirect + github.com/cloudflare/circl v1.6.0 // indirect github.com/fatih/color v1.16.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect @@ -48,7 +48,7 @@ require ( github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/mod v0.22.0 // indirect + golang.org/x/mod v0.24.0 // indirect golang.org/x/net v0.37.0 // indirect golang.org/x/sync v0.13.0 // indirect golang.org/x/sys v0.32.0 // indirect diff --git a/go.sum b/go.sum index 43dd1f5671d..d95c643de4a 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= -github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= @@ -11,10 +11,10 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= -github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= +github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -25,18 +25,18 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= -github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= -github.com/go-git/go-git/v5 v5.13.0 h1:vLn5wlGIh/X78El6r3Jr+30W16Blk0CTcxTYcYPWi5E= -github.com/go-git/go-git/v5 v5.13.0/go.mod h1:Wjo7/JyVKtQgUNdXYXIepzWfJQkUEIGvkvVkiXRR/zw= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= +github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= @@ -70,8 +70,8 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/hc-install v0.9.1 h1:gkqTfE3vVbafGQo6VZXcy2v5yoz2bE0+nhZXruCuODQ= -github.com/hashicorp/hc-install v0.9.1/go.mod h1:pWWvN/IrfeBK4XPeXXYkL6EjMufHkCK5DvwxeLKuBf0= +github.com/hashicorp/hc-install v0.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+OmtO24= +github.com/hashicorp/hc-install v0.9.2/go.mod h1:XUqBQNnuT4RsxoxiM9ZaUk0NX8hi2h+Lb6/c0OZnC/I= github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos= github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= @@ -122,14 +122,14 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= -github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= -github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= @@ -165,8 +165,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= From b2774456b4dca59a421be962420d59513defdf3d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Apr 2025 09:59:22 -0400 Subject: [PATCH 30/53] build(deps): bump github.com/hashicorp/terraform-exec (#1467) Bumps [github.com/hashicorp/terraform-exec](https://github.com/hashicorp/terraform-exec) from 0.22.0 to 0.23.0. - [Release notes](https://github.com/hashicorp/terraform-exec/releases) - [Changelog](https://github.com/hashicorp/terraform-exec/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/terraform-exec/compare/v0.22.0...v0.23.0) --- updated-dependencies: - dependency-name: github.com/hashicorp/terraform-exec dependency-version: 0.23.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 75c6c81d163..9c8c25c2949 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/hashicorp/hc-install v0.9.2 github.com/hashicorp/hcl/v2 v2.23.0 github.com/hashicorp/logutils v1.0.0 - github.com/hashicorp/terraform-exec v0.22.0 + github.com/hashicorp/terraform-exec v0.23.0 github.com/hashicorp/terraform-json v0.24.0 github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1 github.com/hashicorp/terraform-plugin-log v0.9.0 diff --git a/go.sum b/go.sum index d95c643de4a..daaa40e4048 100644 --- a/go.sum +++ b/go.sum @@ -76,8 +76,8 @@ github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3q github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.22.0 h1:G5+4Sz6jYZfRYUCg6eQgDsqTzkNXV+fP8l+uRmZHj64= -github.com/hashicorp/terraform-exec v0.22.0/go.mod h1:bjVbsncaeh8jVdhttWYZuBGj21FcYw6Ia/XfHcNO7lQ= +github.com/hashicorp/terraform-exec v0.23.0 h1:MUiBM1s0CNlRFsCLJuM5wXZrzA3MnPYEsiXmzATMW/I= +github.com/hashicorp/terraform-exec v0.23.0/go.mod h1:mA+qnx1R8eePycfwKkCRk3Wy65mwInvlpAeOwmA7vlY= github.com/hashicorp/terraform-json v0.24.0 h1:rUiyF+x1kYawXeRth6fKFm/MdfBS6+lW4NbeATsYz8Q= github.com/hashicorp/terraform-json v0.24.0/go.mod h1:Nfj5ubo9xbu9uiAoZVBsNOjvNKB66Oyrvtit74kC7ow= github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1 h1:/IZFNUEafGnJGXRe2iNQQ+vtzEw/5qiD+gOxkFrNbi4= From b451f6de85c168baf4bcb18799e87a6a8c0d4c9e Mon Sep 17 00:00:00 2001 From: Ansgar Mertens Date: Fri, 18 Apr 2025 21:42:41 +0200 Subject: [PATCH 31/53] support importing by resource identity (#1463) * support importing by resource identity * add todo note * add tests for ImportResourceState with identity * turn todo into comment * add tests for ImportStateWithIdentity * fix logging_http_transport_test.go after terraform.io started redirecting recently --- helper/logging/logging_http_transport_test.go | 20 +-- helper/schema/grpc_provider.go | 52 +++++- helper/schema/grpc_provider_test.go | 163 ++++++++++++++++++ helper/schema/provider.go | 18 ++ helper/schema/provider_test.go | 158 +++++++++++++++++ helper/schema/resource.go | 2 +- helper/schema/resource_importer.go | 3 + 7 files changed, 403 insertions(+), 13 deletions(-) diff --git a/helper/logging/logging_http_transport_test.go b/helper/logging/logging_http_transport_test.go index a7b84e110dc..73c3515cb60 100644 --- a/helper/logging/logging_http_transport_test.go +++ b/helper/logging/logging_http_transport_test.go @@ -31,7 +31,7 @@ func TestNewLoggingHTTPTransport(t *testing.T) { reqBody := `An example multiline request body` - req, _ := http.NewRequest("GET", "https://www.terraform.io", bytes.NewBufferString(reqBody)) + req, _ := http.NewRequest("GET", "https://developer.hashicorp.com/terraform", bytes.NewBufferString(reqBody)) res, err := client.Do(req.WithContext(ctx)) if err != nil { t.Fatalf("request failed: %v", err) @@ -40,7 +40,7 @@ func TestNewLoggingHTTPTransport(t *testing.T) { entries, err := tflogtest.MultilineJSONDecode(loggerOutput) if err != nil { - t.Fatalf("log outtput parsing failed: %v", err) + t.Fatalf("log output parsing failed: %v", err) } if len(entries) != 2 { @@ -67,12 +67,12 @@ func TestNewLoggingHTTPTransport(t *testing.T) { "@module": "provider", "tf_http_op_type": "request", "tf_http_req_method": "GET", - "tf_http_req_uri": "/", + "tf_http_req_uri": "/terraform", "tf_http_req_version": "HTTP/1.1", "tf_http_req_body": "An example multiline request body", "tf_http_trans_id": transId, "Accept-Encoding": "gzip", - "Host": "www.terraform.io", + "Host": "developer.hashicorp.com", "User-Agent": "Go-http-client/1.1", "Content-Length": "37", }); diff != "" { @@ -122,7 +122,7 @@ func TestNewSubsystemLoggingHTTPTransport(t *testing.T) { reqBody := `An example multiline request body` - req, _ := http.NewRequest("GET", "https://www.terraform.io", bytes.NewBufferString(reqBody)) + req, _ := http.NewRequest("GET", "https://developer.hashicorp.com/terraform", bytes.NewBufferString(reqBody)) res, err := client.Do(req.WithContext(ctx)) if err != nil { t.Fatalf("request failed: %v", err) @@ -158,12 +158,12 @@ func TestNewSubsystemLoggingHTTPTransport(t *testing.T) { "@module": "provider.test-subsystem", "tf_http_op_type": "request", "tf_http_req_method": "GET", - "tf_http_req_uri": "/", + "tf_http_req_uri": "/terraform", "tf_http_req_version": "HTTP/1.1", "tf_http_req_body": "An example multiline request body", "tf_http_trans_id": transId, "Accept-Encoding": "gzip", - "Host": "www.terraform.io", + "Host": "developer.hashicorp.com", "User-Agent": "Go-http-client/1.1", "Content-Length": "37", }); diff != "" { @@ -201,7 +201,7 @@ func TestNewSubsystemLoggingHTTPTransport(t *testing.T) { func TestNewLoggingHTTPTransport_LogMasking(t *testing.T) { ctx, loggerOutput := setupRootLogger() ctx = tflog.MaskFieldValuesWithFieldKeys(ctx, "tf_http_op_type") - ctx = tflog.MaskAllFieldValuesRegexes(ctx, regexp.MustCompile(`(?s).*`)) + ctx = tflog.MaskAllFieldValuesRegexes(ctx, regexp.MustCompile(`(?s).*`)) ctx = tflog.MaskMessageStrings(ctx, "Request", "Response") transport := logging.NewLoggingHTTPTransport(http.DefaultTransport) @@ -210,7 +210,7 @@ func TestNewLoggingHTTPTransport_LogMasking(t *testing.T) { Timeout: 10 * time.Second, } - req, _ := http.NewRequest("GET", "https://www.terraform.io", nil) + req, _ := http.NewRequest("GET", "https://developer.hashicorp.com/terraform", nil) res, err := client.Do(req.WithContext(ctx)) if err != nil { t.Fatalf("request failed: %v", err) @@ -266,7 +266,7 @@ func TestNewLoggingHTTPTransport_LogOmitting(t *testing.T) { Timeout: 10 * time.Second, } - req, _ := http.NewRequest("GET", "https://www.terraform.io", nil) + req, _ := http.NewRequest("GET", "https://developer.hashicorp.com/terraform", nil) res, err := client.Do(req.WithContext(ctx)) if err != nil { t.Fatalf("request failed: %v", err) diff --git a/helper/schema/grpc_provider.go b/helper/schema/grpc_provider.go index 843314f3ba6..c1424f10770 100644 --- a/helper/schema/grpc_provider.go +++ b/helper/schema/grpc_provider.go @@ -1566,7 +1566,27 @@ func (s *GRPCProviderServer) ImportResourceState(ctx context.Context, req *tfpro return resp, nil } - newInstanceStates, err := s.provider.ImportState(ctx, info, req.ID) + var identity map[string]string + // parse identity data if available + if req.Identity != nil && req.Identity.IdentityData != nil { + // convert req.Identity to flat map identity structure + // Step 1: Turn JSON into cty.Value based on schema + identityBlock, err := s.getResourceIdentitySchemaBlock(req.TypeName) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("getting identity schema failed for resource '%s': %w", req.TypeName, err)) + return resp, nil + } + + identityVal, err := msgpack.Unmarshal(req.Identity.IdentityData.MsgPack, identityBlock.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) + return resp, nil + } + // Step 2: Turn cty.Value into flatmap representation + identity = hcl2shim.FlatmapValueFromHCL2(identityVal) + } + + newInstanceStates, err := s.provider.ImportStateWithIdentity(ctx, info, req.ID, identity) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) return resp, nil @@ -1622,12 +1642,40 @@ func (s *GRPCProviderServer) ImportResourceState(ctx context.Context, req *tfpro return resp, nil } + var identityData *tfprotov5.ResourceIdentityData + if is.Identity != nil { + identityBlock, err := s.getResourceIdentitySchemaBlock(resourceType) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("getting identity schema failed for resource '%s': %w", req.TypeName, err)) + return resp, nil + } + + newIdentityVal, err := hcl2shim.HCL2ValueFromFlatmap(is.Identity, identityBlock.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) + return resp, nil + } + + newIdentityMP, err := msgpack.Marshal(newIdentityVal, identityBlock.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) + return resp, nil + } + + identityData = &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: newIdentityMP, + }, + } + } + importedResource := &tfprotov5.ImportedResource{ TypeName: resourceType, State: &tfprotov5.DynamicValue{ MsgPack: newStateMP, }, - Private: meta, + Private: meta, + Identity: identityData, } resp.ImportedResources = append(resp.ImportedResources, importedResource) diff --git a/helper/schema/grpc_provider_test.go b/helper/schema/grpc_provider_test.go index 600a4ade34e..cdf97a30f13 100644 --- a/helper/schema/grpc_provider_test.go +++ b/helper/schema/grpc_provider_test.go @@ -7715,6 +7715,169 @@ func TestImportResourceState(t *testing.T) { }, }, }, + "basic-import-from-identity": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 1, + Schema: map[string]*Schema{ + "id": { + Type: TypeString, + Required: true, + }, + "test_string": { + Type: TypeString, + Computed: true, + }, + }, + Identity: &ResourceIdentity{ + Version: 1, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "id": { + Type: TypeString, + RequiredForImport: true, + }, + } + }, + }, + Importer: &ResourceImporter{ + StateContext: func(ctx context.Context, d *ResourceData, meta interface{}) ([]*ResourceData, error) { + identity, err := d.Identity() + if err != nil { + t.Fatalf("failed to get identity: %v", err) + } + result, exists := identity.GetOk("id") + if !exists { + t.Fatalf("expected id to exist in identity") + } + + err = d.Set("test_string", "new-imported-val") + if err != nil { + return nil, err + } + + d.SetId(result.(string)) + + return []*ResourceData{d}, nil + }, + }, + }, + }, + }), + req: &tfprotov5.ImportResourceStateRequest{ + TypeName: "test", + Identity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("imported-id"), + }), + ), + }, + }, + }, + expected: &tfprotov5.ImportResourceStateResponse{ + ImportedResources: []*tfprotov5.ImportedResource{ + { + TypeName: "test", + State: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test_string": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("imported-id"), + "test_string": cty.StringVal("new-imported-val"), + }), + ), + }, + Private: []byte(`{"schema_version":"1"}`), + Identity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("imported-id"), + }), + ), + }, + }, + }, + }, + }, + }, + "basic-import-from-identity-no-id": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 1, + Schema: map[string]*Schema{ + "id": { + Type: TypeString, + Required: true, + }, + "test_string": { + Type: TypeString, + Computed: true, + }, + }, + Identity: &ResourceIdentity{ + Version: 1, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "id": { + Type: TypeString, + RequiredForImport: true, + }, + } + }, + }, + Importer: &ResourceImporter{ + // Note: this does not set the Id on the ResourceData which results in an error that this test expects + StateContext: func(ctx context.Context, d *ResourceData, meta interface{}) ([]*ResourceData, error) { + err := d.Set("test_string", "new-imported-val") + if err != nil { + return nil, err + } + + return []*ResourceData{d}, nil + }, + }, + }, + }, + }), + req: &tfprotov5.ImportResourceStateRequest{ + TypeName: "test", + Identity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("imported-id"), + }), + ), + }, + }, + }, + expected: &tfprotov5.ImportResourceStateResponse{ + ImportedResources: nil, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "The provider returned a resource missing an identifier during ImportResourceState. This is generally a bug in the resource implementation for import. Resource import code should not call d.SetId(\"\") or create an empty ResourceData. If the resource is missing, instead return an error. Please report this to the provider developers.", + }, + }, + }, + }, "resource-doesnt-exist": { server: NewGRPCProviderServer(&Provider{ ResourcesMap: map[string]*Resource{ diff --git a/helper/schema/provider.go b/helper/schema/provider.go index ad258517eac..cb7e2a89813 100644 --- a/helper/schema/provider.go +++ b/helper/schema/provider.go @@ -476,6 +476,14 @@ func (p *Provider) ImportState( ctx context.Context, info *terraform.InstanceInfo, id string) ([]*terraform.InstanceState, error) { + return p.ImportStateWithIdentity(ctx, info, id, nil) +} + +func (p *Provider) ImportStateWithIdentity( + ctx context.Context, + info *terraform.InstanceInfo, + id string, + identity map[string]string) ([]*terraform.InstanceState, error) { // Find the resource r, ok := p.ResourcesMap[info.Type] if !ok { @@ -492,6 +500,16 @@ func (p *Provider) ImportState( data.SetId(id) data.SetType(info.Type) + if data.identitySchema != nil { + identityData, err := data.Identity() + if err != nil { + return nil, err // this should not happen, as we checked above + } + identityData.raw = identity // is this too hacky / unexpected? + } else if identity != nil { + return nil, fmt.Errorf("resource %s doesn't support identity import", info.Type) + } + // Call the import function results := []*ResourceData{data} if r.Importer.State != nil || r.Importer.StateContext != nil { diff --git a/helper/schema/provider_test.go b/helper/schema/provider_test.go index 0087c61d4c0..2426a6c189a 100644 --- a/helper/schema/provider_test.go +++ b/helper/schema/provider_test.go @@ -2271,6 +2271,164 @@ func TestProviderImportState(t *testing.T) { } } +func TestProviderImportStateWithIdentity(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + provider *Provider + info *terraform.InstanceInfo + id string + identity map[string]string + expectedStates []*terraform.InstanceState + expectedErr error + }{ + "error-missing-identity-schema": { + provider: &Provider{ + ResourcesMap: map[string]*Resource{ + "test_resource": { + Importer: &ResourceImporter{}, + // no identity schema defined + }, + }, + }, + identity: map[string]string{ + "id": "test-id", + }, + info: &terraform.InstanceInfo{ + Type: "test_resource", + }, + expectedErr: fmt.Errorf("resource test_resource doesn't support identity import"), + }, + "error-missing-ResourceData-Id": { + provider: &Provider{ + ResourcesMap: map[string]*Resource{ + "test_resource": { + Importer: &ResourceImporter{ + StateContext: func(_ context.Context, d *ResourceData, _ interface{}) ([]*ResourceData, error) { + // Example for handling import based on identity but not + // setting the id even though it's still required to be set + d.SetId("") + return []*ResourceData{d}, nil + }, + }, + Identity: &ResourceIdentity{ + Version: 1, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "id": { + Type: TypeString, + RequiredForImport: true, + }, + } + }, + }, + }, + }, + }, + info: &terraform.InstanceInfo{ + Type: "test_resource", + }, + identity: map[string]string{ + "id": "test-id", + }, + expectedErr: fmt.Errorf("The provider returned a resource missing an identifier during ImportResourceState."), + }, + "Importer-StateContext-from-identity": { + provider: &Provider{ + ResourcesMap: map[string]*Resource{ + "test_resource": { + Importer: &ResourceImporter{ + StateContext: func(_ context.Context, d *ResourceData, meta interface{}) ([]*ResourceData, error) { + if d.Id() != "" { + return nil, fmt.Errorf("expected d.Id() to be empty, got: %s", d.Id()) + } + + identity, err := d.Identity() + if err != nil { + return nil, fmt.Errorf("error getting identity: %s", err) + } + id, exists := identity.GetOk("id") + if !exists { + return nil, fmt.Errorf("expected identity to contain key id") + } + if id != "test-id" { + return nil, fmt.Errorf("expected identity id %q, got: %s", "test-id", id) + } + + // set region as we act as if it's derived from provider defaults + err = identity.Set("region", "eu-central-1") + if err != nil { + return nil, fmt.Errorf("error setting identity region: %s", err) + } + // set the id as well + d.SetId(id.(string)) + + return []*ResourceData{d}, nil + }, + }, + Identity: &ResourceIdentity{ + Version: 1, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "id": { + Type: TypeString, + RequiredForImport: true, + }, + "region": { + Type: TypeString, + OptionalForImport: true, + }, + } + }, + }, + }, + }, + }, + info: &terraform.InstanceInfo{ + Type: "test_resource", + }, + identity: map[string]string{ + "id": "test-id", + }, + expectedStates: []*terraform.InstanceState{ + { + Attributes: map[string]string{"id": "test-id"}, + Ephemeral: terraform.EphemeralState{Type: "test_resource"}, + ID: "test-id", + Identity: map[string]string{"id": "test-id", "region": "eu-central-1"}, + Meta: map[string]interface{}{"schema_version": "0"}, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + states, err := testCase.provider.ImportStateWithIdentity(context.Background(), testCase.info, testCase.id, testCase.identity) + + if err != nil { + if testCase.expectedErr == nil { + t.Fatalf("unexpected error: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedErr.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedErr, err) + } + } + + if err == nil && testCase.expectedErr != nil { + t.Fatalf("expected error %q, got none", testCase.expectedErr) + } + + if diff := cmp.Diff(states, testCase.expectedStates); diff != "" { + t.Fatalf("unexpected states difference: %s", diff) + } + }) + } +} + func TestProviderMeta(t *testing.T) { p := new(Provider) if v := p.Meta(); v != nil { diff --git a/helper/schema/resource.go b/helper/schema/resource.go index 077a6ddb09b..c206df6c15e 100644 --- a/helper/schema/resource.go +++ b/helper/schema/resource.go @@ -1398,7 +1398,7 @@ func isReservedResourceFieldName(name string) bool { // // This function is useful for unit tests and ResourceImporter functions. func (r *Resource) Data(s *terraform.InstanceState) *ResourceData { - result, err := schemaMap(r.SchemaMap()).Data(s, nil) + result, err := schemaMapWithIdentity{r.SchemaMap(), r.Identity.SchemaMap()}.Data(s, nil) if err != nil { // At the time of writing, this isn't possible (Data never returns // non-nil errors). We panic to find this in the future if we have to. diff --git a/helper/schema/resource_importer.go b/helper/schema/resource_importer.go index ad9a5c3b9c6..e2c4d08b4f7 100644 --- a/helper/schema/resource_importer.go +++ b/helper/schema/resource_importer.go @@ -77,6 +77,9 @@ func ImportStatePassthrough(d *ResourceData, m interface{}) ([]*ResourceData, er // ImportStatePassthroughContext is an implementation of StateContextFunc that can be // used to simply pass the ID directly through. This should be used only // in the case that an ID-only refresh is possible. +// Please note that this implementation does not work when using resource identity as +// an Id still has to be set and the identity might contain multiple fields +// that are not the same as the ID. func ImportStatePassthroughContext(ctx context.Context, d *ResourceData, m interface{}) ([]*ResourceData, error) { return []*ResourceData{d}, nil } From 4d40cabfe55292733e30ead6899294fd99aed95b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Apr 2025 15:43:21 -0400 Subject: [PATCH 32/53] build(deps): bump golang.org/x/net from 0.37.0 to 0.38.0 in /tools (#1469) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.37.0 to 0.38.0. - [Commits](https://github.com/golang/net/compare/v0.37.0...v0.38.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.38.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tools/go.mod | 2 +- tools/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/go.mod b/tools/go.mod index e4ab24aa326..b9c4f8c01da 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -49,7 +49,7 @@ require ( go.mongodb.org/mongo-driver v1.10.0 // indirect golang.org/x/crypto v0.36.0 // indirect golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect - golang.org/x/net v0.37.0 // indirect + golang.org/x/net v0.38.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect golang.org/x/sync v0.12.0 // indirect golang.org/x/sys v0.31.0 // indirect diff --git a/tools/go.sum b/tools/go.sum index d5a7ac1f838..ac09f376cc0 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -411,8 +411,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= From 69259f3787d45156f7fb2fc12ea16fda9ca78fcd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Apr 2025 15:52:24 -0400 Subject: [PATCH 33/53] build(deps): bump golang.org/x/net from 0.37.0 to 0.38.0 (#1470) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.37.0 to 0.38.0. - [Commits](https://github.com/golang/net/compare/v0.37.0...v0.38.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.38.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9c8c25c2949..1f992bf9bc3 100644 --- a/go.mod +++ b/go.mod @@ -49,7 +49,7 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.37.0 // indirect + golang.org/x/net v0.38.0 // indirect golang.org/x/sync v0.13.0 // indirect golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.24.0 // indirect diff --git a/go.sum b/go.sum index daaa40e4048..d45ae2dadc3 100644 --- a/go.sum +++ b/go.sum @@ -171,8 +171,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From c551fba3ddb8378e12e080afb8263845ed41aa8a Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 18 Apr 2025 15:53:21 -0400 Subject: [PATCH 34/53] chore: Add changelog for beta.1 release (#1473) --- .changes/unreleased/NOTES-20250418-154446.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changes/unreleased/NOTES-20250418-154446.yaml diff --git a/.changes/unreleased/NOTES-20250418-154446.yaml b/.changes/unreleased/NOTES-20250418-154446.yaml new file mode 100644 index 00000000000..1006f63b27b --- /dev/null +++ b/.changes/unreleased/NOTES-20250418-154446.yaml @@ -0,0 +1,8 @@ +kind: NOTES +body: This beta pre-release continues the implementation of managed resource identity, which should now be used with Terraform v1.12.0-beta2. + Managed resources now can support import by identity during plan and apply workflows. Managed resources that already support import via the + `schema.Resource.Importer` field still need to set an ID during import when an identity is provided. The `RequiredForImport` and + `OptionalForImport` fields on the identity schema can be used to control the validation that Terraform core will apply to the import config block. +time: 2025-04-18T15:44:46.554826-04:00 +custom: + Issue: "1463" From 2571b118d7c23830fe52ab8b728aae627576cf52 Mon Sep 17 00:00:00 2001 From: hc-github-team-tf-provider-devex Date: Fri, 18 Apr 2025 19:54:41 +0000 Subject: [PATCH 35/53] Update meta package SDKVersion --- meta/meta.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meta/meta.go b/meta/meta.go index 39a187a1af7..9555d134c93 100644 --- a/meta/meta.go +++ b/meta/meta.go @@ -25,7 +25,7 @@ var SDKVersion = "2.37.0" // // Deprecated: Use Go standard library [runtime/debug] package build information // instead. -var SDKPrerelease = "alpha.1" +var SDKPrerelease = "beta.1" // SemVer is an instance of version.Version. This has the secondary // benefit of verifying during tests and init time that our version is a From 698af5e1f591676310648a92fe50ec62340e3cce Mon Sep 17 00:00:00 2001 From: hc-github-team-tf-provider-devex Date: Fri, 18 Apr 2025 19:54:53 +0000 Subject: [PATCH 36/53] Update changelog --- .changes/2.37.0-beta.1.md | 6 ++++++ .changes/unreleased/NOTES-20250418-154446.yaml | 8 -------- CHANGELOG.md | 6 ++++++ 3 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 .changes/2.37.0-beta.1.md delete mode 100644 .changes/unreleased/NOTES-20250418-154446.yaml diff --git a/.changes/2.37.0-beta.1.md b/.changes/2.37.0-beta.1.md new file mode 100644 index 00000000000..f79f32ca76d --- /dev/null +++ b/.changes/2.37.0-beta.1.md @@ -0,0 +1,6 @@ +## 2.37.0-beta.1 (April 18, 2025) + +NOTES: + +* This beta pre-release continues the implementation of managed resource identity, which should now be used with Terraform v1.12.0-beta2. Managed resources now can support import by identity during plan and apply workflows. Managed resources that already support import via the `schema.Resource.Importer` field still need to set an ID during import when an identity is provided. The `RequiredForImport` and `OptionalForImport` fields on the identity schema can be used to control the validation that Terraform core will apply to the import config block. ([#1463](https://github.com/hashicorp/terraform-plugin-sdk/issues/1463)) + diff --git a/.changes/unreleased/NOTES-20250418-154446.yaml b/.changes/unreleased/NOTES-20250418-154446.yaml deleted file mode 100644 index 1006f63b27b..00000000000 --- a/.changes/unreleased/NOTES-20250418-154446.yaml +++ /dev/null @@ -1,8 +0,0 @@ -kind: NOTES -body: This beta pre-release continues the implementation of managed resource identity, which should now be used with Terraform v1.12.0-beta2. - Managed resources now can support import by identity during plan and apply workflows. Managed resources that already support import via the - `schema.Resource.Importer` field still need to set an ID during import when an identity is provided. The `RequiredForImport` and - `OptionalForImport` fields on the identity schema can be used to control the validation that Terraform core will apply to the import config block. -time: 2025-04-18T15:44:46.554826-04:00 -custom: - Issue: "1463" diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c325b4625a..1847bacfa34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.37.0-beta.1 (April 18, 2025) + +NOTES: + +* This beta pre-release continues the implementation of managed resource identity, which should now be used with Terraform v1.12.0-beta2. Managed resources now can support import by identity during plan and apply workflows. Managed resources that already support import via the `schema.Resource.Importer` field still need to set an ID during import when an identity is provided. The `RequiredForImport` and `OptionalForImport` fields on the identity schema can be used to control the validation that Terraform core will apply to the import config block. ([#1463](https://github.com/hashicorp/terraform-plugin-sdk/issues/1463)) + ## 2.37.0-alpha.1 (March 20, 2025) NOTES: From 6aefc6f2b4da9bef86cd148b03c6ce6c8eff5c08 Mon Sep 17 00:00:00 2001 From: yutachaos <18604471+yutachaos@users.noreply.github.com> Date: Mon, 21 Apr 2025 19:55:14 +0900 Subject: [PATCH 37/53] Added test for StringLenBetween (#1386) --- helper/validation/strings_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/helper/validation/strings_test.go b/helper/validation/strings_test.go index 9068c4f72f3..1bb78630a0a 100644 --- a/helper/validation/strings_test.go +++ b/helper/validation/strings_test.go @@ -243,6 +243,25 @@ func TestValidationStringIsWhiteSpace(t *testing.T) { } } +func TestValidationStringLenBetween(t *testing.T) { + runTestCases(t, []testCase{ + { + val: "abc", + f: StringLenBetween(1, 5), + }, + { + val: "InvalidValue", + f: StringLenBetween(1, 5), + expectedErr: regexp.MustCompile(`expected length of [\w]+ to be in the range \(1 \- 5\), got InvalidValue`), + }, + { + val: 1, + f: StringLenBetween(1, 5), + expectedErr: regexp.MustCompile(`expected type of [\w]+ to be string`), + }, + }) +} + func TestValidationStringIsBase64(t *testing.T) { cases := map[string]struct { Value interface{} From 34b82717dd28fba04e273f6e89049ff3b81d29b3 Mon Sep 17 00:00:00 2001 From: Ansgar Mertens Date: Wed, 23 Apr 2025 14:18:21 +0200 Subject: [PATCH 38/53] TFECO-9286: add `ImportStatePassthroughWithIdentity` (#1474) * add ImportStatePassthroughWithIdentity * remove obsolete comment * add documentation comment and one more test case --- helper/schema/provider.go | 2 +- helper/schema/resource_importer.go | 47 ++++++ helper/schema/resource_importer_test.go | 189 +++++++++++++++++++++++- 3 files changed, 236 insertions(+), 2 deletions(-) diff --git a/helper/schema/provider.go b/helper/schema/provider.go index cb7e2a89813..28c39bb0492 100644 --- a/helper/schema/provider.go +++ b/helper/schema/provider.go @@ -505,7 +505,7 @@ func (p *Provider) ImportStateWithIdentity( if err != nil { return nil, err // this should not happen, as we checked above } - identityData.raw = identity // is this too hacky / unexpected? + identityData.raw = identity } else if identity != nil { return nil, fmt.Errorf("resource %s doesn't support identity import", info.Type) } diff --git a/helper/schema/resource_importer.go b/helper/schema/resource_importer.go index e2c4d08b4f7..4dd691d2269 100644 --- a/helper/schema/resource_importer.go +++ b/helper/schema/resource_importer.go @@ -6,6 +6,7 @@ package schema import ( "context" "errors" + "fmt" ) // ResourceImporter defines how a resource is imported in Terraform. This @@ -83,3 +84,49 @@ func ImportStatePassthrough(d *ResourceData, m interface{}) ([]*ResourceData, er func ImportStatePassthroughContext(ctx context.Context, d *ResourceData, m interface{}) ([]*ResourceData, error) { return []*ResourceData{d}, nil } + +// ImportStatePassthroughWithIdentity creates a StateContextFunc that supports both +// identity-based and ID-only resource import scenarios. This function is useful +// when a resource can be imported either by its unique ID or by an identity attribute. +// +// The `idAttributePath` parameter specifies the name of the identity attribute +// to use when importing by identity. Since identity attributes are "flat", +// `idAttributePath` should be a simple attribute name (e.g., "name" or "identifier"). +// Note that the identity attribute must be a string, as this function expects +// to set the resource ID using the value of the specified attribute. +// +// If the resource is imported by ID (i.e., `d.Id()` is already set), the function +// simply returns the resource data as-is. Otherwise, it attempts to retrieve the +// identity attribute specified by `idAttributePath` and sets it as the resource ID. +// +// Parameters: +// - idAttributePath: The name of the identity attribute to use for setting the ID. +// +// Returns: +// - A StateContextFunc that handles the import logic. +func ImportStatePassthroughWithIdentity(idAttributePath string) StateContextFunc { + return func(ctx context.Context, d *ResourceData, m interface{}) ([]*ResourceData, error) { + // If we import by id, we just return the resource data as is, no need to change it + if d.Id() != "" { + return []*ResourceData{d}, nil + } + + // If we import by identity, we need to set the id based on the idAttributePath + identity, err := d.Identity() + if err != nil { + return nil, fmt.Errorf("error getting identity: %s", err) + } + id, exists := identity.GetOk(idAttributePath) + if !exists { + return nil, fmt.Errorf("expected identity to contain key %s", idAttributePath) + } + idStr, ok := id.(string) + if !ok { + return nil, fmt.Errorf("expected identity key %s to be a string, was: %T", idAttributePath, id) + } + + d.SetId(idStr) + + return []*ResourceData{d}, nil + } +} diff --git a/helper/schema/resource_importer_test.go b/helper/schema/resource_importer_test.go index 36e4256a0bd..6a08634c1de 100644 --- a/helper/schema/resource_importer_test.go +++ b/helper/schema/resource_importer_test.go @@ -3,7 +3,11 @@ package schema -import "testing" +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) func TestInternalValidate(t *testing.T) { r := &ResourceImporter{ @@ -14,3 +18,186 @@ func TestInternalValidate(t *testing.T) { t.Fatal("ResourceImporter should not allow State and StateContext to be set") } } + +func TestImportStatePassthroughWithIdentity(t *testing.T) { + // shared among all tests, defined once to keep them shorter + identitySchema := map[string]*Schema{ + "email": { + Type: TypeString, + RequiredForImport: true, + }, + "region": { + Type: TypeString, + OptionalForImport: true, + }, + } + + tests := []struct { + name string + idAttributePath string + resourceData *ResourceData + expectedResourceData *ResourceData + expectedError string + }{ + { + name: "import from id just sets id", + idAttributePath: "email", + resourceData: &ResourceData{ + identitySchema: identitySchema, + state: &terraform.InstanceState{ + ID: "hello@example.internal", + }, + }, + expectedResourceData: &ResourceData{ + identitySchema: identitySchema, + state: &terraform.InstanceState{ + ID: "hello@example.internal", + }, + }, + }, + { + name: "import from identity sets id and identity", + idAttributePath: "email", + resourceData: &ResourceData{ + identitySchema: identitySchema, + state: &terraform.InstanceState{ + Identity: map[string]string{ + "email": "hello@example.internal", + }, + }, + }, + expectedResourceData: &ResourceData{ + identitySchema: identitySchema, + state: &terraform.InstanceState{ + ID: "hello@example.internal", + }, + newIdentity: &IdentityData{ + schema: identitySchema, + raw: map[string]string{ + "email": "hello@example.internal", + }, + }, + }, + }, + { + name: "import from identity sets id and identity (with region set)", + idAttributePath: "email", + resourceData: &ResourceData{ + identitySchema: identitySchema, + state: &terraform.InstanceState{ + Identity: map[string]string{ + "email": "hello@example.internal", + "region": "eu-west-1", + }, + }, + }, + expectedResourceData: &ResourceData{ + identitySchema: identitySchema, + state: &terraform.InstanceState{ + ID: "hello@example.internal", + }, + newIdentity: &IdentityData{ + schema: identitySchema, + raw: map[string]string{ + "email": "hello@example.internal", + "region": "eu-west-1", + }, + }, + }, + }, + { + name: "import from identity fails without required field", + idAttributePath: "email", + resourceData: &ResourceData{ + identitySchema: identitySchema, + state: &terraform.InstanceState{ + Identity: map[string]string{ + "region": "eu-west-1", + }, + }, + }, + expectedError: "expected identity to contain key email", + }, + { + name: "import from identity fails if attribute is not a string", + idAttributePath: "number", + resourceData: &ResourceData{ + identitySchema: map[string]*Schema{ + "number": { + Type: TypeInt, + RequiredForImport: true, + }, + }, + state: &terraform.InstanceState{ + Identity: map[string]string{ + "number": "1", + }, + }, + }, + expectedError: "expected identity key number to be a string, was: int", + }, + { + name: "import from identity fails without schema", + idAttributePath: "email", + resourceData: &ResourceData{ + state: &terraform.InstanceState{ + Identity: map[string]string{ + "email": "hello@example.internal", + }, + }, + }, + expectedError: "error getting identity: Resource does not have Identity schema. Please set one in order to use Identity(). This is always a problem in the provider code.", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + results, err := ImportStatePassthroughWithIdentity(test.idAttributePath)(nil, test.resourceData, nil) + if err != nil { + if test.expectedError == "" { + t.Fatalf("unexpected error: %s", err) + } + if err.Error() != test.expectedError { + t.Fatalf("expected error: %s, got: %s", test.expectedError, err) + } + return // we don't expect any results if there is an error + } + if len(results) != 1 { + t.Fatalf("expected 1 result, got: %d", len(results)) + } + // compare id and identity in resource data + if results[0].Id() != test.expectedResourceData.Id() { + t.Fatalf("expected id: %s, got: %s", test.expectedResourceData.Id(), results[0].Id()) + } + // compare identity + expectedIdentity, err := test.expectedResourceData.Identity() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + resultIdentity, err := results[0].Identity() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + // check whether all result identity attributes exist as expected + for key := range expectedIdentity.schema { + expected := expectedIdentity.getRaw(key) + if expected.Exists { + result := resultIdentity.getRaw(key) + if !result.Exists { + t.Fatalf("expected identity attribute %s to exist", key) + } + if expected.Value != result.Value { + t.Fatalf("expected identity attribute %s to be %s, got: %s", key, expected.Value, result.Value) + } + } + } + // check whether there are no additional attributes in the result identity + for key := range resultIdentity.schema { + if _, ok := expectedIdentity.schema[key]; !ok { + t.Fatalf("unexpected identity attribute %s", key) + } + } + }) + } +} From 151f59e3965b7b1b4ee7ea1da7c0d236548a5ce0 Mon Sep 17 00:00:00 2001 From: Ansgar Mertens Date: Thu, 24 Apr 2025 09:30:51 +0200 Subject: [PATCH 39/53] add TestResourceDataWithIdentityRaw test helper (#1475) Resolves #1464 --- helper/schema/testing.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/helper/schema/testing.go b/helper/schema/testing.go index bdf56d9012f..202f8a31f60 100644 --- a/helper/schema/testing.go +++ b/helper/schema/testing.go @@ -30,3 +30,20 @@ func TestResourceDataRaw(t testing.T, schema map[string]*Schema, raw map[string] return result } + +// TestResourceDataWithIdentityRaw creates a ResourceData with an identity from a raw identity map. +func TestResourceDataWithIdentityRaw(t testing.T, schema map[string]*Schema, identitySchema map[string]*Schema, raw map[string]string) *ResourceData { + t.Helper() + + sm := schemaMapWithIdentity{schema, identitySchema} + state := terraform.InstanceState{ + Identity: raw, + } + + result, err := sm.Data(&state, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + return result +} From 031cb9006287655f40ea227134d68c85b73e2cf9 Mon Sep 17 00:00:00 2001 From: Mauricio Alvarez Leon <65101411+BBBmau@users.noreply.github.com> Date: Thu, 24 Apr 2025 10:22:09 -0700 Subject: [PATCH 40/53] add writeOnly in updateFuncSet check (#1472) --- helper/schema/resource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helper/schema/resource.go b/helper/schema/resource.go index c206df6c15e..0bf7f9597d0 100644 --- a/helper/schema/resource.go +++ b/helper/schema/resource.go @@ -1216,7 +1216,7 @@ func (r *Resource) InternalValidate(topSchemaMap schemaMap, writable bool) error if !r.updateFuncSet() { nonForceNewAttrs := make([]string, 0) for k, v := range schema { - if !v.ForceNew && !v.Computed { + if !v.ForceNew && !v.Computed && !v.WriteOnly { nonForceNewAttrs = append(nonForceNewAttrs, k) } } From 0584e8adf3f23d5c1949a1c2aa8ba4119e0331de Mon Sep 17 00:00:00 2001 From: Mauricio Alvarez Leon <65101411+BBBmau@users.noreply.github.com> Date: Thu, 24 Apr 2025 10:25:31 -0700 Subject: [PATCH 41/53] add writeOnly in updateFuncSet check (#1472) From 9b4eafc0a918909bb186d0efbd59de13a64a91b3 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Mon, 5 May 2025 09:50:08 -0400 Subject: [PATCH 42/53] Add changie entry for write-only update method validation bugfix (#1476) --- .changes/unreleased/BUG FIXES-20250424-133143.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changes/unreleased/BUG FIXES-20250424-133143.yaml diff --git a/.changes/unreleased/BUG FIXES-20250424-133143.yaml b/.changes/unreleased/BUG FIXES-20250424-133143.yaml new file mode 100644 index 00000000000..c98ec929bcc --- /dev/null +++ b/.changes/unreleased/BUG FIXES-20250424-133143.yaml @@ -0,0 +1,5 @@ +kind: BUG FIXES +body: 'helper/schema: Fixed bug that blocked write-only attributes from being used with resources without update functions.' +time: 2025-04-24T13:31:43.847209-04:00 +custom: + Issue: "1472" From a8969de4a3fbaefcc00d902ad26abec51c424fa1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 09:35:45 -0400 Subject: [PATCH 43/53] build(deps): bump golang.org/x/crypto from 0.37.0 to 0.38.0 (#1477) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.37.0 to 0.38.0. - [Commits](https://github.com/golang/crypto/compare/v0.37.0...v0.38.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.38.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 1f992bf9bc3..d62c985aac4 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/reflectwalk v1.0.2 github.com/zclconf/go-cty v1.16.2 - golang.org/x/crypto v0.37.0 + golang.org/x/crypto v0.38.0 ) require ( @@ -50,9 +50,9 @@ require ( github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect golang.org/x/mod v0.24.0 // indirect golang.org/x/net v0.38.0 // indirect - golang.org/x/sync v0.13.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/text v0.24.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect diff --git a/go.sum b/go.sum index d45ae2dadc3..effb3e6c32f 100644 --- a/go.sum +++ b/go.sum @@ -162,8 +162,8 @@ go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= @@ -176,8 +176,8 @@ golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -190,18 +190,18 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= From 5e69f976be069a65f21b4c03e543b13d0b4970be Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 13 May 2025 08:14:38 -0400 Subject: [PATCH 44/53] Add catalog metadata (META.d) (#1480) * Add catalog metadata (META.d) * Add catalog metadata (META.d) --- .copywrite.hcl | 3 +++ META.d/_summary.yaml | 10 ++++++++++ 2 files changed, 13 insertions(+) create mode 100644 META.d/_summary.yaml diff --git a/.copywrite.hcl b/.copywrite.hcl index d755ab73b18..680f44c94f1 100644 --- a/.copywrite.hcl +++ b/.copywrite.hcl @@ -5,6 +5,9 @@ project { copyright_year = 2019 header_ignore = [ + # internal catalog metadata (prose) + "META.d/**/*.yaml", + # changie tooling configuration and CHANGELOG entries (prose) ".changes/unreleased/**", ".changie.yaml", diff --git a/META.d/_summary.yaml b/META.d/_summary.yaml new file mode 100644 index 00000000000..7af04096053 --- /dev/null +++ b/META.d/_summary.yaml @@ -0,0 +1,10 @@ +--- +schema: 1.1 + +partition: tf-ecosystem + +summary: + owner: team-tf-core-plugins + description: | + Terraform Plugin SDK enables building plugins (providers) to manage any service providers or custom in-house solutions + visibility: public From d40c432fa99d3d5048cb854e817e563b8a9c31a4 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 13 May 2025 10:03:18 -0400 Subject: [PATCH 45/53] Prep changelogs for `v2.37.0` release (#1479) * move go upgrade changelog over * add changelogs --- .../unreleased/ENHANCEMENTS-20250512-171131.yaml | 6 ++++++ .../unreleased/ENHANCEMENTS-20250512-171419.yaml | 5 +++++ .changes/unreleased/FEATURES-20250512-170956.yaml | 5 +++++ .changes/unreleased/FEATURES-20250512-171620.yaml | 5 +++++ .changes/unreleased/FEATURES-20250512-172448.yaml | 5 +++++ .../{upcoming-stable => }/NOTES-20250317-152749.yaml | 0 .changes/unreleased/NOTES-20250512-171756.yaml | 12 ++++++++++++ 7 files changed, 38 insertions(+) create mode 100644 .changes/unreleased/ENHANCEMENTS-20250512-171131.yaml create mode 100644 .changes/unreleased/ENHANCEMENTS-20250512-171419.yaml create mode 100644 .changes/unreleased/FEATURES-20250512-170956.yaml create mode 100644 .changes/unreleased/FEATURES-20250512-171620.yaml create mode 100644 .changes/unreleased/FEATURES-20250512-172448.yaml rename .changes/unreleased/{upcoming-stable => }/NOTES-20250317-152749.yaml (100%) create mode 100644 .changes/unreleased/NOTES-20250512-171756.yaml diff --git a/.changes/unreleased/ENHANCEMENTS-20250512-171131.yaml b/.changes/unreleased/ENHANCEMENTS-20250512-171131.yaml new file mode 100644 index 00000000000..d50d527d127 --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20250512-171131.yaml @@ -0,0 +1,6 @@ +kind: ENHANCEMENTS +body: 'helper/schema: Added `RequiredForImport` and `OptionalForImport` fields to the `Schema` struct, which + are only valid for identity schemas.' +time: 2025-05-12T17:11:31.101353-04:00 +custom: + Issue: "1444" diff --git a/.changes/unreleased/ENHANCEMENTS-20250512-171419.yaml b/.changes/unreleased/ENHANCEMENTS-20250512-171419.yaml new file mode 100644 index 00000000000..abd40d6f53b --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20250512-171419.yaml @@ -0,0 +1,5 @@ +kind: ENHANCEMENTS +body: 'helper/schema: Updated `ResourceData` to support passing of identity data in CRUD and import functions for managed resources.' +time: 2025-05-12T17:14:19.893664-04:00 +custom: + Issue: "1444" diff --git a/.changes/unreleased/FEATURES-20250512-170956.yaml b/.changes/unreleased/FEATURES-20250512-170956.yaml new file mode 100644 index 00000000000..6974651724c --- /dev/null +++ b/.changes/unreleased/FEATURES-20250512-170956.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'helper/schema: Added new `TestResourceDataWithIdentityRaw` function for creating a `ResourceData` struct with identity data for unit testing.' +time: 2025-05-12T17:09:56.812031-04:00 +custom: + Issue: "1475" diff --git a/.changes/unreleased/FEATURES-20250512-171620.yaml b/.changes/unreleased/FEATURES-20250512-171620.yaml new file mode 100644 index 00000000000..867378b68d7 --- /dev/null +++ b/.changes/unreleased/FEATURES-20250512-171620.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'helper/schema: Added new `Identity` field to `Resource` that supports defining an identity schema for managed resources only.' +time: 2025-05-12T17:16:20.82808-04:00 +custom: + Issue: "1444" diff --git a/.changes/unreleased/FEATURES-20250512-172448.yaml b/.changes/unreleased/FEATURES-20250512-172448.yaml new file mode 100644 index 00000000000..4b9f0fd6ef0 --- /dev/null +++ b/.changes/unreleased/FEATURES-20250512-172448.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: Added new `ImportStatePassthroughWithIdentity` helper that can support both identity and ID importing via a single field. +time: 2025-05-12T17:24:48.004385-04:00 +custom: + Issue: "1474" diff --git a/.changes/unreleased/upcoming-stable/NOTES-20250317-152749.yaml b/.changes/unreleased/NOTES-20250317-152749.yaml similarity index 100% rename from .changes/unreleased/upcoming-stable/NOTES-20250317-152749.yaml rename to .changes/unreleased/NOTES-20250317-152749.yaml diff --git a/.changes/unreleased/NOTES-20250512-171756.yaml b/.changes/unreleased/NOTES-20250512-171756.yaml new file mode 100644 index 00000000000..07ced3b34a1 --- /dev/null +++ b/.changes/unreleased/NOTES-20250512-171756.yaml @@ -0,0 +1,12 @@ +kind: NOTES +body: 'all: This release contains new fields and structs for implmenting managed resource identity. + Resource identity is data that is defined by a separate schema and is stored alongside resource state. + Identity data is used by Terrform to uniquely identify a remote object and is meant to be immutable during + the remote object''s lifecycle. Resources that support identity can now be imported using the `identity` attribute + in Terraform configuration `import` blocks, available in Terraform v1.12+. The `resource.Identity` field on the `schema.Resource` + struct can be used to support identity by defining an identity schema. Once the identity schema is defined, you can read + and store identity data in the state file with the new `IdentityData` struct that is available via the `Identity()` method + on `schema.ResourceData` and `schema.ResourceDiff` structs.' +time: 2025-05-12T17:17:56.934967-04:00 +custom: + Issue: "1444" From 104e5510474809be19ddb5d39d62489a2a106d50 Mon Sep 17 00:00:00 2001 From: Ansgar Mertens Date: Thu, 15 May 2025 12:22:55 +0200 Subject: [PATCH 46/53] ResourceIdentity: Validate that identities do not change after Terraform stores it (#1478) * Validate that identities do not change after Terraform stores it * introduce MutableIdentity resource behaviour and remove obsolete todos * add tests for MutableIdentity resource behaviour * address review feedback --- helper/schema/grpc_provider.go | 105 +- helper/schema/grpc_provider_test.go | 3364 ++++++++++++++++++++------- helper/schema/resource.go | 5 + terraform/state.go | 7 + 4 files changed, 2651 insertions(+), 830 deletions(-) diff --git a/helper/schema/grpc_provider.go b/helper/schema/grpc_provider.go index c1424f10770..e6334e924ea 100644 --- a/helper/schema/grpc_provider.go +++ b/helper/schema/grpc_provider.go @@ -159,7 +159,7 @@ func (s *GRPCProviderServer) UpgradeResourceIdentity(ctx context.Context, req *t // now we need to turn the state into the default json representation, so // that it can be re-decoded using the actual schema. - val, err := JSONMapToStateValue(jsonMap, schemaBlock) // TODO: Find out if we need resource identity version here + val, err := JSONMapToStateValue(jsonMap, schemaBlock) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) return resp, nil @@ -788,11 +788,43 @@ func (s *GRPCProviderServer) ConfigureProvider(ctx context.Context, req *tfproto func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.ReadResourceRequest) (*tfprotov5.ReadResourceResponse, error) { ctx = logging.InitContext(ctx) + readFollowingImport := false + + reqPrivate := req.Private + + if reqPrivate != nil { + // unmarshal the private data + if len(reqPrivate) > 0 { + newReqPrivate := make(map[string]interface{}) + if err := json.Unmarshal(reqPrivate, &newReqPrivate); err != nil { + return nil, err + } + // This internal private field is set on a resource during ImportResourceState to help framework determine if + // the resource has been recently imported. We only need to read this once, so we immediately clear it after. + if _, ok := newReqPrivate[terraform.ImportBeforeReadMetaKey]; ok { + readFollowingImport = true + delete(newReqPrivate, terraform.ImportBeforeReadMetaKey) + + if len(newReqPrivate) == 0 { + // if there are no other private data, set the private data to nil + reqPrivate = nil + } else { + // set the new private data without the import key + bytes, err := json.Marshal(newReqPrivate) + if err != nil { + return nil, err + } + reqPrivate = bytes + } + } + } + } + resp := &tfprotov5.ReadResourceResponse{ // helper/schema did previously handle private data during refresh, but // core is now going to expect this to be maintained in order to // persist it in the state. - Private: req.Private, + Private: reqPrivate, } res, ok := s.provider.ResourcesMap[req.TypeName] @@ -832,7 +864,7 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re } instanceState.RawState = stateVal - // TODO: is there a more elegant way to do this? this requires us to look for the identity schema block again + var currentIdentityVal cty.Value if req.CurrentIdentity != nil && req.CurrentIdentity.IdentityData != nil { // convert req.CurrentIdentity to flat map identity structure @@ -843,20 +875,20 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re return resp, nil } - identityVal, err := msgpack.Unmarshal(req.CurrentIdentity.IdentityData.MsgPack, identityBlock.ImpliedType()) + currentIdentityVal, err = msgpack.Unmarshal(req.CurrentIdentity.IdentityData.MsgPack, identityBlock.ImpliedType()) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) return resp, nil } // Step 2: Turn cty.Value into flatmap representation - identityAttrs := hcl2shim.FlatmapValueFromHCL2(identityVal) + identityAttrs := hcl2shim.FlatmapValueFromHCL2(currentIdentityVal) // Step 3: Well, set it in the instanceState instanceState.Identity = identityAttrs } private := make(map[string]interface{}) - if len(req.Private) > 0 { - if err := json.Unmarshal(req.Private, &private); err != nil { + if len(reqPrivate) > 0 { + if err := json.Unmarshal(reqPrivate, &private); err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) return resp, nil } @@ -929,6 +961,15 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re return resp, nil } + // If we're refreshing the resource state (excluding a recently imported resource), validate that the new identity isn't changing + if !res.ResourceBehavior.MutableIdentity && !readFollowingImport && !currentIdentityVal.IsNull() && !currentIdentityVal.RawEquals(newIdentityVal) { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("Unexpected Identity Change: %s", "During the read operation, the Terraform Provider unexpectedly returned a different identity then the previously stored one.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.\n\n"+ + fmt.Sprintf("Current Identity: %s\n\n", currentIdentityVal.GoString())+ + fmt.Sprintf("New Identity: %s", newIdentityVal.GoString()))) + return resp, nil + } + newIdentityMP, err := msgpack.Marshal(newIdentityVal, identityBlock.ImpliedType()) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) @@ -1052,6 +1093,7 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot // turn the proposed state into a legacy configuration cfg := terraform.NewResourceConfigShimmed(proposedNewStateVal, schemaBlock) + var priorIdentityVal cty.Value // add identity data to priorState if req.PriorIdentity != nil && req.PriorIdentity.IdentityData != nil { // convert req.PriorIdentity to flat map identity structure @@ -1062,13 +1104,13 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot return resp, nil } - identityVal, err := msgpack.Unmarshal(req.PriorIdentity.IdentityData.MsgPack, identityBlock.ImpliedType()) + priorIdentityVal, err = msgpack.Unmarshal(req.PriorIdentity.IdentityData.MsgPack, identityBlock.ImpliedType()) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) return resp, nil } // Step 2: Turn cty.Value into flatmap representation - identityAttrs := hcl2shim.FlatmapValueFromHCL2(identityVal) + identityAttrs := hcl2shim.FlatmapValueFromHCL2(priorIdentityVal) // Step 3: Well, set it in the priorState priorState.Identity = identityAttrs } @@ -1088,7 +1130,6 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot diff.Attributes["id"] = &terraform.ResourceAttrDiff{ NewComputed: true, } - // TODO: we could error here if a new Diff got no Identity set } if diff == nil || (len(diff.Attributes) == 0 && len(diff.Identity) == 0) { @@ -1249,7 +1290,6 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot } } - // TODO: if schema defines identity, we should error if there's none written to newInstanceState if res.Identity != nil { identityBlock, err := s.getResourceIdentitySchemaBlock(req.TypeName) if err != nil { @@ -1257,13 +1297,26 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot return resp, nil } - newIdentityVal, err := hcl2shim.HCL2ValueFromFlatmap(diff.Identity, identityBlock.ImpliedType()) + plannedIdentityVal, err := hcl2shim.HCL2ValueFromFlatmap(diff.Identity, identityBlock.ImpliedType()) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) return resp, nil } - newIdentityMP, err := msgpack.Marshal(newIdentityVal, identityBlock.ImpliedType()) + // If we're updating or deleting and we already have an identity stored, validate that the planned identity isn't changing + if !res.ResourceBehavior.MutableIdentity && !create && !priorIdentityVal.IsNull() && !priorIdentityVal.RawEquals(plannedIdentityVal) { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf( + "Unexpected Identity Change: During the planning operation, the Terraform Provider unexpectedly returned a different identity than the previously stored one.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.\n\n"+ + "Prior Identity: %s\n\nPlanned Identity: %s", + priorIdentityVal.GoString(), + plannedIdentityVal.GoString(), + )) + + return resp, nil + } + + plannedIdentityMP, err := msgpack.Marshal(plannedIdentityVal, identityBlock.ImpliedType()) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) return resp, nil @@ -1271,7 +1324,7 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot resp.PlannedIdentity = &tfprotov5.ResourceIdentityData{ IdentityData: &tfprotov5.DynamicValue{ - MsgPack: newIdentityMP, + MsgPack: plannedIdentityMP, }, } } @@ -1299,6 +1352,8 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro return resp, nil } + create := priorStateVal.IsNull() + plannedStateVal, err := msgpack.Unmarshal(req.PlannedState.MsgPack, schemaBlock.ImpliedType()) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) @@ -1325,6 +1380,7 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro } } + var plannedIdentityVal cty.Value // add identity data to priorState if req.PlannedIdentity != nil && req.PlannedIdentity.IdentityData != nil { // convert req.PriorIdentity to flat map identity structure @@ -1335,13 +1391,13 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro return resp, nil } - identityVal, err := msgpack.Unmarshal(req.PlannedIdentity.IdentityData.MsgPack, identityBlock.ImpliedType()) + plannedIdentityVal, err = msgpack.Unmarshal(req.PlannedIdentity.IdentityData.MsgPack, identityBlock.ImpliedType()) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) return resp, nil } // Step 2: Turn cty.Value into flatmap representation - identityAttrs := hcl2shim.FlatmapValueFromHCL2(identityVal) + identityAttrs := hcl2shim.FlatmapValueFromHCL2(plannedIdentityVal) // Step 3: Well, set it in the priorState priorState.Identity = identityAttrs } @@ -1475,7 +1531,6 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro } resp.Private = meta - // TODO: if schema defines identity, we should error if there's none written to newInstanceState if res.Identity != nil { identityBlock, err := s.getResourceIdentitySchemaBlock(req.TypeName) if err != nil { @@ -1489,6 +1544,18 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro return resp, nil } + if !res.ResourceBehavior.MutableIdentity && !create && !plannedIdentityVal.IsNull() && !plannedIdentityVal.RawEquals(newIdentityVal) { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf( + "Unexpected Identity Change: During the update operation, the Terraform Provider unexpectedly returned a different identity than the previously stored one.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.\n\n"+ + "Planned Identity: %s\n\nNew Identity: %s", + plannedIdentityVal.GoString(), + newIdentityVal.GoString(), + )) + + return resp, nil + } + newIdentityMP, err := msgpack.Marshal(newIdentityVal, identityBlock.ImpliedType()) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) @@ -1636,6 +1703,10 @@ func (s *GRPCProviderServer) ImportResourceState(ctx context.Context, req *tfpro return resp, nil } + // Set an internal private field that will get sent alongside the imported resource. This will be cleared by + // the following ReadResource RPC and is primarily used to control validation of resource identities during refresh. + is.Meta[terraform.ImportBeforeReadMetaKey] = true + meta, err := json.Marshal(is.Meta) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) diff --git a/helper/schema/grpc_provider_test.go b/helper/schema/grpc_provider_test.go index cdf97a30f13..ab15715a951 100644 --- a/helper/schema/grpc_provider_test.go +++ b/helper/schema/grpc_provider_test.go @@ -5076,6 +5076,9 @@ func TestReadResource(t *testing.T) { Computed: true, }, }, + ResourceBehavior: ResourceBehavior{ + MutableIdentity: true, + }, Identity: &ResourceIdentity{ Version: 1, SchemaFunc: func() map[string]*Schema { @@ -5432,585 +5435,615 @@ func TestReadResource(t *testing.T) { }, }, }, - } - - for name, testCase := range testCases { - t.Run(name, func(t *testing.T) { - t.Parallel() - resp, err := testCase.server.ReadResource(context.Background(), testCase.req) - - if err != nil { - t.Fatal(err) - } - - if diff := cmp.Diff(resp, testCase.expected, valueComparer); diff != "" { - ty := testCase.server.getResourceSchemaBlock("test").ImpliedType() - - if resp != nil && resp.NewState != nil { - t.Logf("resp.NewState.MsgPack: %s", mustMsgpackUnmarshal(ty, resp.NewState.MsgPack)) - } - - if testCase.expected != nil && testCase.expected.NewState != nil { - t.Logf("expected: %s", mustMsgpackUnmarshal(ty, testCase.expected.NewState.MsgPack)) - } - - t.Error(diff) - } - }) - } -} - -func TestPlanResourceChange(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - server *GRPCProviderServer - req *tfprotov5.PlanResourceChangeRequest - expected *tfprotov5.PlanResourceChangeResponse - }{ - "basic-plan": { + "update-resource-without-prior-identity-identity-may-change": { server: NewGRPCProviderServer(&Provider{ ResourcesMap: map[string]*Resource{ "test": { - SchemaVersion: 4, + SchemaVersion: 1, Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - Optional: true, + "id": { + Type: TypeString, + Required: true, + }, + "test": { + Type: TypeString, + }, + }, + Identity: &ResourceIdentity{ + Version: 1, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "identity": { + Type: TypeString, + RequiredForImport: true, + }, + } }, }, + ReadContext: func(ctx context.Context, d *ResourceData, meta interface{}) diag.Diagnostics { + identity, err := d.Identity() + if err != nil { + return diag.FromErr(err) + } + err = identity.Set("identity", "changed") + if err != nil { + return diag.FromErr(err) + } + + return nil + }, }, }, }), - req: &tfprotov5.PlanResourceChangeRequest{ - TypeName: "test", - PriorState: &tfprotov5.DynamicValue{ - MsgPack: mustMsgpackMarshal( - cty.Object(map[string]cty.Type{ - "foo": cty.Number, - }), - cty.NullVal( - cty.Object(map[string]cty.Type{ - "foo": cty.Number, - }), - ), - ), - }, - ProposedNewState: &tfprotov5.DynamicValue{ - MsgPack: mustMsgpackMarshal( - cty.Object(map[string]cty.Type{ - "id": cty.String, - "foo": cty.Number, - }), - cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "foo": cty.NullVal(cty.Number), - }), - ), - }, - Config: &tfprotov5.DynamicValue{ + req: &tfprotov5.ReadResourceRequest{ + TypeName: "test", + CurrentIdentity: nil, // no prior identity because previous provider version didn't support it yet + CurrentState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "foo": cty.Number, + "test": cty.String, + "id": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.NullVal(cty.String), - "foo": cty.NullVal(cty.Number), + "test": cty.StringVal("hello"), + "id": cty.StringVal("initial"), }), ), }, }, - expected: &tfprotov5.PlanResourceChangeResponse{ - PlannedState: &tfprotov5.DynamicValue{ + expected: &tfprotov5.ReadResourceResponse{ + NewState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "foo": cty.Number, + "id": cty.String, + "test": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "foo": cty.NullVal(cty.Number), + "id": cty.StringVal("initial"), + "test": cty.StringVal("hello"), }), ), }, - RequiresReplace: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("id"), + NewIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "identity": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "identity": cty.StringVal("changed"), + }), + ), + }, }, - PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), - UnsafeToUseLegacyTypeSystem: true, }, }, - "basic-plan-with-identity": { + "imported-resource-by-identity-identity-may-change": { server: NewGRPCProviderServer(&Provider{ ResourcesMap: map[string]*Resource{ "test": { - SchemaVersion: 4, + SchemaVersion: 1, Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - Optional: true, + "id": { + Type: TypeString, + Required: true, + }, + "test": { + Type: TypeString, }, }, Identity: &ResourceIdentity{ Version: 1, SchemaFunc: func() map[string]*Schema { return map[string]*Schema{ - "name": { + "identity": { Type: TypeString, RequiredForImport: true, }, } }, }, + ReadContext: func(ctx context.Context, d *ResourceData, meta interface{}) diag.Diagnostics { + err := d.Set("test", "hello") + if err != nil { + return diag.FromErr(err) + } + + identity, err := d.Identity() + if err != nil { + return diag.FromErr(err) + } + err = identity.Set("identity", "changed") + if err != nil { + return diag.FromErr(err) + } + + return nil + }, }, }, }), - req: &tfprotov5.PlanResourceChangeRequest{ + req: &tfprotov5.ReadResourceRequest{ TypeName: "test", - PriorState: &tfprotov5.DynamicValue{ - MsgPack: mustMsgpackMarshal( - cty.Object(map[string]cty.Type{ - "foo": cty.Number, - }), - cty.NullVal( + CurrentIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "foo": cty.Number, + "identity": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "identity": cty.StringVal("initial"), }), ), - ), - }, - ProposedNewState: &tfprotov5.DynamicValue{ - MsgPack: mustMsgpackMarshal( - cty.Object(map[string]cty.Type{ - "id": cty.String, - "foo": cty.Number, - }), - cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "foo": cty.NullVal(cty.Number), - }), - ), + }, }, - Config: &tfprotov5.DynamicValue{ + Private: []byte(`{".import_before_read":true}`), + CurrentState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "foo": cty.Number, + "id": cty.String, + "test": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.NullVal(cty.String), - "foo": cty.NullVal(cty.Number), + "id": cty.StringVal("initial"), + "test": cty.UnknownVal(cty.String), }), ), }, - PriorIdentity: &tfprotov5.ResourceIdentityData{ - IdentityData: &tfprotov5.DynamicValue{ - MsgPack: mustMsgpackMarshal( - cty.Object(map[string]cty.Type{ - "name": cty.String, - }), - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("test-name"), - }), - ), - }, - }, }, - expected: &tfprotov5.PlanResourceChangeResponse{ - PlannedState: &tfprotov5.DynamicValue{ + expected: &tfprotov5.ReadResourceResponse{ + NewState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "foo": cty.Number, + "id": cty.String, + "test": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "foo": cty.NullVal(cty.Number), + "id": cty.StringVal("initial"), + "test": cty.StringVal("hello"), }), ), }, - RequiresReplace: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("id"), - }, - PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), - UnsafeToUseLegacyTypeSystem: true, - PlannedIdentity: &tfprotov5.ResourceIdentityData{ + NewIdentity: &tfprotov5.ResourceIdentityData{ IdentityData: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "name": cty.String, + "identity": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("test-name"), + "identity": cty.StringVal("changed"), }), ), }, }, }, }, - "new-resource-with-identity": { + "imported-resource-by-id-identity-may-change": { server: NewGRPCProviderServer(&Provider{ ResourcesMap: map[string]*Resource{ "test": { - SchemaVersion: 4, + SchemaVersion: 1, Schema: map[string]*Schema{ - "foo": { + "id": { Type: TypeString, - Optional: true, + Required: true, + }, + "test": { + Type: TypeString, }, }, Identity: &ResourceIdentity{ Version: 1, SchemaFunc: func() map[string]*Schema { return map[string]*Schema{ - "name": { + "identity": { Type: TypeString, RequiredForImport: true, }, } }, }, - CustomizeDiff: func(ctx context.Context, d *ResourceDiff, meta interface{}) error { + ReadContext: func(ctx context.Context, d *ResourceData, meta interface{}) diag.Diagnostics { + err := d.Set("test", "hello") + if err != nil { + return diag.FromErr(err) + } + identity, err := d.Identity() if err != nil { - return err + return diag.FromErr(err) } - err = identity.Set("name", "Peter") + err = identity.Set("identity", "changed") if err != nil { - return err + return diag.FromErr(err) } + return nil }, }, }, }), - req: &tfprotov5.PlanResourceChangeRequest{ - TypeName: "test", - PriorState: &tfprotov5.DynamicValue{ + req: &tfprotov5.ReadResourceRequest{ + TypeName: "test", + CurrentIdentity: nil, + Private: []byte(`{".import_before_read":true}`), + CurrentState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "foo": cty.String, + "id": cty.String, + "test": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "foo": cty.StringVal("baz"), - }), - ), - }, - ProposedNewState: &tfprotov5.DynamicValue{ - MsgPack: mustMsgpackMarshal( - cty.Object(map[string]cty.Type{ - "id": cty.String, - "foo": cty.String, - }), - cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "foo": cty.StringVal("baz"), - }), - ), - }, - Config: &tfprotov5.DynamicValue{ - MsgPack: mustMsgpackMarshal( - cty.Object(map[string]cty.Type{ - "id": cty.String, - "foo": cty.String, - }), - cty.ObjectVal(map[string]cty.Value{ - "id": cty.NullVal(cty.String), - "foo": cty.StringVal("baz"), + "id": cty.StringVal("initial"), + "test": cty.UnknownVal(cty.String), }), ), }, }, - expected: &tfprotov5.PlanResourceChangeResponse{ - PlannedState: &tfprotov5.DynamicValue{ + expected: &tfprotov5.ReadResourceResponse{ + NewState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "foo": cty.String, + "id": cty.String, + "test": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "foo": cty.StringVal("baz"), + "id": cty.StringVal("initial"), + "test": cty.StringVal("hello"), }), ), }, - RequiresReplace: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("id"), - }, - PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), - UnsafeToUseLegacyTypeSystem: true, - PlannedIdentity: &tfprotov5.ResourceIdentityData{ + NewIdentity: &tfprotov5.ResourceIdentityData{ IdentityData: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "name": cty.String, + "identity": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("Peter"), + "identity": cty.StringVal("changed"), }), ), }, }, }, }, - "no identity schema": { + "update-resource-identity-may-not-change": { server: NewGRPCProviderServer(&Provider{ ResourcesMap: map[string]*Resource{ "test": { - SchemaVersion: 4, + SchemaVersion: 1, Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - Optional: true, + "id": { + Type: TypeString, + Required: true, + }, + "test": { + Type: TypeString, }, }, Identity: &ResourceIdentity{ Version: 1, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "identity": { + Type: TypeString, + RequiredForImport: true, + }, + } + }, + }, + ReadContext: func(ctx context.Context, d *ResourceData, meta interface{}) diag.Diagnostics { + identity, err := d.Identity() + if err != nil { + return diag.FromErr(err) + } + err = identity.Set("identity", "changed") + if err != nil { + return diag.FromErr(err) + } + + return nil }, }, }, }), - req: &tfprotov5.PlanResourceChangeRequest{ + req: &tfprotov5.ReadResourceRequest{ TypeName: "test", - PriorState: &tfprotov5.DynamicValue{ - MsgPack: mustMsgpackMarshal( - cty.Object(map[string]cty.Type{ - "foo": cty.Number, - }), - cty.NullVal( + CurrentIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "foo": cty.Number, + "identity": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "identity": cty.StringVal("initial"), }), ), - ), + }, }, - ProposedNewState: &tfprotov5.DynamicValue{ + CurrentState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "foo": cty.Number, + "test": cty.String, + "id": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "foo": cty.NullVal(cty.Number), + "test": cty.StringVal("hello"), + "id": cty.StringVal("initial"), }), ), }, - Config: &tfprotov5.DynamicValue{ + }, + expected: &tfprotov5.ReadResourceResponse{ + NewState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "foo": cty.Number, + "id": cty.String, + "test": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.NullVal(cty.String), - "foo": cty.NullVal(cty.Number), + "id": cty.StringVal("initial"), + "test": cty.StringVal("hello"), }), ), }, - PriorIdentity: &tfprotov5.ResourceIdentityData{ - IdentityData: &tfprotov5.DynamicValue{ - MsgPack: mustMsgpackMarshal( - cty.Object(map[string]cty.Type{ - "name": cty.String, - }), - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("test-name"), - }), - ), - }, - }, - }, - expected: &tfprotov5.PlanResourceChangeResponse{ Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, - Summary: "getting identity schema failed for resource 'test': resource does not have an identity schema", + Summary: (`Unexpected Identity Change: During the read operation, the Terraform Provider unexpectedly returned a different identity then the previously stored one. + +This is always a problem with the provider and should be reported to the provider developer. + +Current Identity: cty.ObjectVal(map[string]cty.Value{"identity":cty.StringVal("initial")}) + +New Identity: cty.ObjectVal(map[string]cty.Value{"identity":cty.StringVal("changed")})`), }, }, - UnsafeToUseLegacyTypeSystem: true, }, }, - "empty identity schema": { + "update-resource-identity-may-change-if-mutable-identity-allowed": { server: NewGRPCProviderServer(&Provider{ ResourcesMap: map[string]*Resource{ "test": { - SchemaVersion: 4, + ResourceBehavior: ResourceBehavior{ + MutableIdentity: true, + }, + SchemaVersion: 1, Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - Optional: true, + "id": { + Type: TypeString, + Required: true, + }, + "test": { + Type: TypeString, }, }, Identity: &ResourceIdentity{ Version: 1, SchemaFunc: func() map[string]*Schema { - return map[string]*Schema{} + return map[string]*Schema{ + "identity": { + Type: TypeString, + RequiredForImport: true, + }, + } }, }, + ReadContext: func(ctx context.Context, d *ResourceData, meta interface{}) diag.Diagnostics { + identity, err := d.Identity() + if err != nil { + return diag.FromErr(err) + } + err = identity.Set("identity", "changed") + if err != nil { + return diag.FromErr(err) + } + + return nil + }, }, }, }), - req: &tfprotov5.PlanResourceChangeRequest{ + req: &tfprotov5.ReadResourceRequest{ TypeName: "test", - PriorState: &tfprotov5.DynamicValue{ - MsgPack: mustMsgpackMarshal( - cty.Object(map[string]cty.Type{ - "foo": cty.Number, - }), - cty.NullVal( + CurrentIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "foo": cty.Number, + "identity": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "identity": cty.StringVal("initial"), }), ), - ), + }, }, - ProposedNewState: &tfprotov5.DynamicValue{ + CurrentState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "foo": cty.Number, + "test": cty.String, + "id": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "foo": cty.NullVal(cty.Number), + "test": cty.StringVal("hello"), + "id": cty.StringVal("initial"), }), ), }, - Config: &tfprotov5.DynamicValue{ + }, + expected: &tfprotov5.ReadResourceResponse{ + NewState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "foo": cty.Number, + "id": cty.String, + "test": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.NullVal(cty.String), - "foo": cty.NullVal(cty.Number), + "id": cty.StringVal("initial"), + "test": cty.StringVal("hello"), }), ), }, - PriorIdentity: &tfprotov5.ResourceIdentityData{ + NewIdentity: &tfprotov5.ResourceIdentityData{ IdentityData: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "name": cty.String, + "identity": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("test-name"), + "identity": cty.StringVal("changed"), }), ), }, }, }, - expected: &tfprotov5.PlanResourceChangeResponse{ - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "getting identity schema failed for resource 'test': identity schema must have at least one attribute", - }, - }, - UnsafeToUseLegacyTypeSystem: true, - }, }, - "basic-plan-EnableLegacyTypeSystemPlanErrors": { + "does-not-remove-user-data-from-private": { server: NewGRPCProviderServer(&Provider{ ResourcesMap: map[string]*Resource{ "test": { - // Will set UnsafeToUseLegacyTypeSystem to false - EnableLegacyTypeSystemPlanErrors: true, + SchemaVersion: 1, Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - Optional: true, + "id": { + Type: TypeString, + Required: true, + }, + "test": { + Type: TypeString, + }, + }, + Identity: &ResourceIdentity{ + Version: 1, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "identity": { + Type: TypeString, + RequiredForImport: true, + }, + } }, }, + ReadContext: func(ctx context.Context, d *ResourceData, meta interface{}) diag.Diagnostics { + err := d.Set("test", "hello") + if err != nil { + return diag.FromErr(err) + } + + identity, err := d.Identity() + if err != nil { + return diag.FromErr(err) + } + err = identity.Set("identity", "changed") + if err != nil { + return diag.FromErr(err) + } + + return nil + }, }, }, }), - req: &tfprotov5.PlanResourceChangeRequest{ + req: &tfprotov5.ReadResourceRequest{ TypeName: "test", - PriorState: &tfprotov5.DynamicValue{ - MsgPack: mustMsgpackMarshal( - cty.Object(map[string]cty.Type{ - "foo": cty.Number, - }), - cty.NullVal( + CurrentIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "foo": cty.Number, + "identity": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "identity": cty.StringVal("initial"), }), ), - ), + }, }, - ProposedNewState: &tfprotov5.DynamicValue{ + Private: []byte(`{".import_before_read":true,"user_defined_key":"user_defined_value"}`), + CurrentState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "foo": cty.Number, - }), - cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "foo": cty.NullVal(cty.Number), - }), - ), - }, - Config: &tfprotov5.DynamicValue{ - MsgPack: mustMsgpackMarshal( - cty.Object(map[string]cty.Type{ - "id": cty.String, - "foo": cty.Number, + "id": cty.String, + "test": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.NullVal(cty.String), - "foo": cty.NullVal(cty.Number), + "id": cty.StringVal("initial"), + "test": cty.UnknownVal(cty.String), }), ), }, }, - expected: &tfprotov5.PlanResourceChangeResponse{ - PlannedState: &tfprotov5.DynamicValue{ + expected: &tfprotov5.ReadResourceResponse{ + NewState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "foo": cty.Number, + "id": cty.String, + "test": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "foo": cty.NullVal(cty.Number), + "id": cty.StringVal("initial"), + "test": cty.StringVal("hello"), }), ), }, - RequiresReplace: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("id"), + Private: []byte(`{"user_defined_key":"user_defined_value"}`), + NewIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "identity": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "identity": cty.StringVal("changed"), + }), + ), + }, }, - PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), - UnsafeToUseLegacyTypeSystem: false, }, }, - "deferred-with-provider-plan-modification": { + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + resp, err := testCase.server.ReadResource(context.Background(), testCase.req) + + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(resp, testCase.expected, valueComparer); diff != "" { + ty := testCase.server.getResourceSchemaBlock("test").ImpliedType() + + if resp != nil && resp.NewState != nil { + t.Logf("resp.NewState.MsgPack: %s", mustMsgpackUnmarshal(ty, resp.NewState.MsgPack)) + } + + if testCase.expected != nil && testCase.expected.NewState != nil { + t.Logf("expected: %s", mustMsgpackUnmarshal(ty, testCase.expected.NewState.MsgPack)) + } + + t.Error(diff) + } + }) + } +} + +func TestPlanResourceChange(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + server *GRPCProviderServer + req *tfprotov5.PlanResourceChangeRequest + expected *tfprotov5.PlanResourceChangeResponse + }{ + "basic-plan": { server: NewGRPCProviderServer(&Provider{ - providerDeferred: &Deferred{ - Reason: DeferredReasonProviderConfigUnknown, - }, ResourcesMap: map[string]*Resource{ "test": { - ResourceBehavior: ResourceBehavior{ - ProviderDeferred: ProviderDeferredBehavior{ - // Will ensure that CustomizeDiff is called - EnablePlanModification: true, - }, - }, SchemaVersion: 4, - CustomizeDiff: func(ctx context.Context, d *ResourceDiff, i interface{}) error { - return d.SetNew("foo", "new-foo-value") - }, Schema: map[string]*Schema{ "foo": { - Type: TypeString, + Type: TypeInt, Optional: true, - Computed: true, }, }, }, @@ -6018,17 +6051,14 @@ func TestPlanResourceChange(t *testing.T) { }), req: &tfprotov5.PlanResourceChangeRequest{ TypeName: "test", - ClientCapabilities: &tfprotov5.PlanResourceChangeClientCapabilities{ - DeferralAllowed: true, - }, PriorState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "foo": cty.String, + "foo": cty.Number, }), cty.NullVal( cty.Object(map[string]cty.Type{ - "foo": cty.String, + "foo": cty.Number, }), ), ), @@ -6037,11 +6067,11 @@ func TestPlanResourceChange(t *testing.T) { MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ "id": cty.String, - "foo": cty.String, + "foo": cty.Number, }), cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), - "foo": cty.UnknownVal(cty.String), + "foo": cty.NullVal(cty.Number), }), ), }, @@ -6049,28 +6079,25 @@ func TestPlanResourceChange(t *testing.T) { MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ "id": cty.String, - "foo": cty.String, + "foo": cty.Number, }), cty.ObjectVal(map[string]cty.Value{ "id": cty.NullVal(cty.String), - "foo": cty.NullVal(cty.String), + "foo": cty.NullVal(cty.Number), }), ), }, }, expected: &tfprotov5.PlanResourceChangeResponse{ - Deferred: &tfprotov5.Deferred{ - Reason: tfprotov5.DeferredReasonProviderConfigUnknown, - }, PlannedState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ "id": cty.String, - "foo": cty.String, + "foo": cty.Number, }), cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), - "foo": cty.StringVal("new-foo-value"), + "foo": cty.NullVal(cty.Number), }), ), }, @@ -6081,22 +6108,26 @@ func TestPlanResourceChange(t *testing.T) { UnsafeToUseLegacyTypeSystem: true, }, }, - "deferred-skip-plan-modification": { + "basic-plan-with-identity": { server: NewGRPCProviderServer(&Provider{ - providerDeferred: &Deferred{ - Reason: DeferredReasonProviderConfigUnknown, - }, ResourcesMap: map[string]*Resource{ "test": { SchemaVersion: 4, - CustomizeDiff: func(ctx context.Context, d *ResourceDiff, i interface{}) error { - return errors.New("Test assertion failed: CustomizeDiff shouldn't be called") - }, Schema: map[string]*Schema{ "foo": { - Type: TypeString, + Type: TypeInt, Optional: true, - Computed: true, + }, + }, + Identity: &ResourceIdentity{ + Version: 1, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "name": { + Type: TypeString, + RequiredForImport: true, + }, + } }, }, }, @@ -6104,17 +6135,14 @@ func TestPlanResourceChange(t *testing.T) { }), req: &tfprotov5.PlanResourceChangeRequest{ TypeName: "test", - ClientCapabilities: &tfprotov5.PlanResourceChangeClientCapabilities{ - DeferralAllowed: true, - }, PriorState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "foo": cty.String, + "foo": cty.Number, }), cty.NullVal( cty.Object(map[string]cty.Type{ - "foo": cty.String, + "foo": cty.Number, }), ), ), @@ -6123,11 +6151,11 @@ func TestPlanResourceChange(t *testing.T) { MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ "id": cty.String, - "foo": cty.String, + "foo": cty.Number, }), cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), - "foo": cty.StringVal("from-config!"), + "foo": cty.NullVal(cty.Number), }), ), }, @@ -6135,55 +6163,92 @@ func TestPlanResourceChange(t *testing.T) { MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ "id": cty.String, - "foo": cty.String, + "foo": cty.Number, }), cty.ObjectVal(map[string]cty.Value{ "id": cty.NullVal(cty.String), - "foo": cty.StringVal("from-config!"), + "foo": cty.NullVal(cty.Number), }), ), }, + PriorIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "name": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("test-name"), + }), + ), + }, + }, }, expected: &tfprotov5.PlanResourceChangeResponse{ - Deferred: &tfprotov5.Deferred{ - Reason: tfprotov5.DeferredReasonProviderConfigUnknown, - }, - // Returns proposed new state with deferred response PlannedState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ "id": cty.String, - "foo": cty.String, + "foo": cty.Number, }), cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), - "foo": cty.StringVal("from-config!"), + "foo": cty.NullVal(cty.Number), }), ), }, + RequiresReplace: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("id"), + }, + PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), UnsafeToUseLegacyTypeSystem: true, + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "name": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("test-name"), + }), + ), + }, + }, }, }, - "create: write-only value can be retrieved in CustomizeDiff": { + "new-resource-with-identity": { server: NewGRPCProviderServer(&Provider{ ResourcesMap: map[string]*Resource{ "test": { SchemaVersion: 4, - CustomizeDiff: func(ctx context.Context, d *ResourceDiff, i interface{}) error { - val := d.Get("foo") - if val != "bar" { - t.Fatalf("Incorrect write-only value") - } - - return nil - }, Schema: map[string]*Schema{ "foo": { - Type: TypeString, - Optional: true, - WriteOnly: true, + Type: TypeString, + Optional: true, + }, + }, + Identity: &ResourceIdentity{ + Version: 1, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "name": { + Type: TypeString, + RequiredForImport: true, + }, + } }, }, + CustomizeDiff: func(ctx context.Context, d *ResourceDiff, meta interface{}) error { + identity, err := d.Identity() + if err != nil { + return err + } + err = identity.Set("name", "Peter") + if err != nil { + return err + } + return nil + }, }, }, }), @@ -6192,13 +6257,13 @@ func TestPlanResourceChange(t *testing.T) { PriorState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ + "id": cty.String, "foo": cty.String, }), - cty.NullVal( - cty.Object(map[string]cty.Type{ - "foo": cty.String, - }), - ), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.StringVal("baz"), + }), ), }, ProposedNewState: &tfprotov5.DynamicValue{ @@ -6209,7 +6274,7 @@ func TestPlanResourceChange(t *testing.T) { }), cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), - "foo": cty.StringVal("bar"), + "foo": cty.StringVal("baz"), }), ), }, @@ -6221,7 +6286,7 @@ func TestPlanResourceChange(t *testing.T) { }), cty.ObjectVal(map[string]cty.Value{ "id": cty.NullVal(cty.String), - "foo": cty.StringVal("bar"), + "foo": cty.StringVal("baz"), }), ), }, @@ -6235,34 +6300,43 @@ func TestPlanResourceChange(t *testing.T) { }), cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), - "foo": cty.NullVal(cty.String), + "foo": cty.StringVal("baz"), }), ), }, - PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), RequiresReplace: []*tftypes.AttributePath{ tftypes.NewAttributePath().WithAttributeName("id"), }, + PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), UnsafeToUseLegacyTypeSystem: true, + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "name": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("Peter"), + }), + ), + }, + }, }, }, - "create: write-only values are nullified in PlanResourceChangeResponse": { + "no identity schema": { server: NewGRPCProviderServer(&Provider{ ResourcesMap: map[string]*Resource{ "test": { SchemaVersion: 4, Schema: map[string]*Schema{ "foo": { - Type: TypeString, - Optional: true, - WriteOnly: true, - }, - "bar": { - Type: TypeString, - Optional: true, - WriteOnly: true, + Type: TypeInt, + Optional: true, }, }, + Identity: &ResourceIdentity{ + Version: 1, + }, }, }, }), @@ -6271,13 +6345,11 @@ func TestPlanResourceChange(t *testing.T) { PriorState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "foo": cty.String, - "bar": cty.String, + "foo": cty.Number, }), cty.NullVal( cty.Object(map[string]cty.Type{ - "foo": cty.String, - "bar": cty.String, + "foo": cty.Number, }), ), ), @@ -6286,13 +6358,11 @@ func TestPlanResourceChange(t *testing.T) { MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ "id": cty.String, - "foo": cty.String, - "bar": cty.String, + "foo": cty.Number, }), cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), - "foo": cty.StringVal("baz"), - "bar": cty.StringVal("boop"), + "foo": cty.NullVal(cty.Number), }), ), }, @@ -6300,61 +6370,52 @@ func TestPlanResourceChange(t *testing.T) { MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ "id": cty.String, - "foo": cty.String, - "bar": cty.String, + "foo": cty.Number, }), cty.ObjectVal(map[string]cty.Value{ "id": cty.NullVal(cty.String), - "foo": cty.StringVal("baz"), - "bar": cty.StringVal("boop"), + "foo": cty.NullVal(cty.Number), }), ), }, + PriorIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "name": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("test-name"), + }), + ), + }, + }, }, expected: &tfprotov5.PlanResourceChangeResponse{ - PlannedState: &tfprotov5.DynamicValue{ - MsgPack: mustMsgpackMarshal( - cty.Object(map[string]cty.Type{ - "id": cty.String, - "foo": cty.String, - "bar": cty.String, - }), - cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "foo": cty.NullVal(cty.String), - "bar": cty.NullVal(cty.String), - }), - ), - }, - PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), - RequiresReplace: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("id"), + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "getting identity schema failed for resource 'test': resource does not have an identity schema", + }, }, UnsafeToUseLegacyTypeSystem: true, }, }, - "update: write-only value can be retrieved in CustomizeDiff": { + "empty identity schema": { server: NewGRPCProviderServer(&Provider{ ResourcesMap: map[string]*Resource{ "test": { SchemaVersion: 4, - CustomizeDiff: func(ctx context.Context, d *ResourceDiff, i interface{}) error { - val := d.Get("write_only") - if val != "bar" { - t.Fatalf("Incorrect write-only value") - } - - return nil - }, Schema: map[string]*Schema{ - "configured": { - Type: TypeString, + "foo": { + Type: TypeInt, Optional: true, }, - "write_only": { - Type: TypeString, - Optional: true, - WriteOnly: true, + }, + Identity: &ResourceIdentity{ + Version: 1, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{} }, }, }, @@ -6365,88 +6426,73 @@ func TestPlanResourceChange(t *testing.T) { PriorState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "configured": cty.String, - "write_only": cty.String, - }), - cty.ObjectVal(map[string]cty.Value{ - "id": cty.NullVal(cty.String), - "configured": cty.StringVal("prior_val"), - "write_only": cty.NullVal(cty.String), + "foo": cty.Number, }), + cty.NullVal( + cty.Object(map[string]cty.Type{ + "foo": cty.Number, + }), + ), ), }, ProposedNewState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "configured": cty.String, - "write_only": cty.String, + "id": cty.String, + "foo": cty.Number, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "configured": cty.StringVal("updated_val"), - "write_only": cty.StringVal("bar"), + "id": cty.UnknownVal(cty.String), + "foo": cty.NullVal(cty.Number), }), ), }, Config: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "configured": cty.String, - "write_only": cty.String, + "id": cty.String, + "foo": cty.Number, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.NullVal(cty.String), - "configured": cty.StringVal("updated_val"), - "write_only": cty.StringVal("bar"), + "id": cty.NullVal(cty.String), + "foo": cty.NullVal(cty.Number), }), ), }, + PriorIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "name": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("test-name"), + }), + ), + }, + }, }, expected: &tfprotov5.PlanResourceChangeResponse{ - PlannedState: &tfprotov5.DynamicValue{ - MsgPack: mustMsgpackMarshal( - cty.Object(map[string]cty.Type{ - "id": cty.String, - "configured": cty.String, - "write_only": cty.String, - }), - cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "configured": cty.StringVal("updated_val"), - "write_only": cty.NullVal(cty.String), - }), - ), - }, - PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), - RequiresReplace: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("id"), + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "getting identity schema failed for resource 'test': identity schema must have at least one attribute", + }, }, UnsafeToUseLegacyTypeSystem: true, }, }, - "update: write-only values are nullified in PlanResourceChangeResponse": { + "basic-plan-EnableLegacyTypeSystemPlanErrors": { server: NewGRPCProviderServer(&Provider{ ResourcesMap: map[string]*Resource{ "test": { - SchemaVersion: 4, + // Will set UnsafeToUseLegacyTypeSystem to false + EnableLegacyTypeSystemPlanErrors: true, Schema: map[string]*Schema{ - "configured": { - Type: TypeString, + "foo": { + Type: TypeInt, Optional: true, }, - "write_onlyA": { - Type: TypeString, - Optional: true, - WriteOnly: true, - }, - "write_onlyB": { - Type: TypeString, - Optional: true, - WriteOnly: true, - }, }, }, }, @@ -6456,48 +6502,36 @@ func TestPlanResourceChange(t *testing.T) { PriorState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "configured": cty.String, - "write_onlyA": cty.String, - "write_onlyB": cty.String, - }), - cty.ObjectVal(map[string]cty.Value{ - "id": cty.NullVal(cty.String), - "configured": cty.StringVal("prior_val"), - "write_onlyA": cty.NullVal(cty.String), - "write_onlyB": cty.NullVal(cty.String), + "foo": cty.Number, }), + cty.NullVal( + cty.Object(map[string]cty.Type{ + "foo": cty.Number, + }), + ), ), }, ProposedNewState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "configured": cty.String, - "write_onlyA": cty.String, - "write_onlyB": cty.String, + "id": cty.String, + "foo": cty.Number, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "configured": cty.StringVal("updated_val"), - "write_onlyA": cty.StringVal("foo"), - "write_onlyB": cty.StringVal("bar"), + "id": cty.UnknownVal(cty.String), + "foo": cty.NullVal(cty.Number), }), ), }, Config: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "configured": cty.String, - "write_onlyA": cty.String, - "write_onlyB": cty.String, + "id": cty.String, + "foo": cty.Number, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.NullVal(cty.String), - "configured": cty.StringVal("updated_val"), - "write_onlyA": cty.StringVal("foo"), - "write_onlyB": cty.StringVal("bar"), + "id": cty.NullVal(cty.String), + "foo": cty.NullVal(cty.Number), }), ), }, @@ -6506,193 +6540,75 @@ func TestPlanResourceChange(t *testing.T) { PlannedState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "configured": cty.String, - "write_onlyA": cty.String, - "write_onlyB": cty.String, + "id": cty.String, + "foo": cty.Number, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "configured": cty.StringVal("updated_val"), - "write_onlyA": cty.NullVal(cty.String), - "write_onlyB": cty.NullVal(cty.String), + "id": cty.UnknownVal(cty.String), + "foo": cty.NullVal(cty.Number), }), ), }, - PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), RequiresReplace: []*tftypes.AttributePath{ tftypes.NewAttributePath().WithAttributeName("id"), }, - UnsafeToUseLegacyTypeSystem: true, + PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), + UnsafeToUseLegacyTypeSystem: false, }, }, - } - - for name, testCase := range testCases { - t.Run(name, func(t *testing.T) { - t.Parallel() - - resp, err := testCase.server.PlanResourceChange(context.Background(), testCase.req) - if err != nil { - t.Fatal(err) - } - - if diff := cmp.Diff(resp, testCase.expected, valueComparer); diff != "" { - ty := testCase.server.getResourceSchemaBlock("test").ImpliedType() - - if resp != nil && resp.PlannedState != nil { - t.Logf("resp.PlannedState.MsgPack: %s", mustMsgpackUnmarshal(ty, resp.PlannedState.MsgPack)) - } - - if testCase.expected != nil && testCase.expected.PlannedState != nil { - t.Logf("expected: %s", mustMsgpackUnmarshal(ty, testCase.expected.PlannedState.MsgPack)) - } - - t.Error(diff) - } - }) - } -} - -func TestPlanResourceChange_bigint(t *testing.T) { - r := &Resource{ - UseJSONNumber: true, - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - Required: true, - }, - }, - } - - server := NewGRPCProviderServer(&Provider{ - ResourcesMap: map[string]*Resource{ - "test": r, - }, - }) - - schema := r.CoreConfigSchema() - priorState, err := msgpack.Marshal(cty.NullVal(schema.ImpliedType()), schema.ImpliedType()) - if err != nil { - t.Fatal(err) - } - - proposedVal := cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "foo": cty.MustParseNumberVal("7227701560655103598"), - }) - proposedState, err := msgpack.Marshal(proposedVal, schema.ImpliedType()) - if err != nil { - t.Fatal(err) - } - - config, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{ - "id": cty.NullVal(cty.String), - "foo": cty.MustParseNumberVal("7227701560655103598"), - })) - if err != nil { - t.Fatal(err) - } - configBytes, err := msgpack.Marshal(config, schema.ImpliedType()) - if err != nil { - t.Fatal(err) - } - - testReq := &tfprotov5.PlanResourceChangeRequest{ - TypeName: "test", - PriorState: &tfprotov5.DynamicValue{ - MsgPack: priorState, - }, - ProposedNewState: &tfprotov5.DynamicValue{ - MsgPack: proposedState, - }, - Config: &tfprotov5.DynamicValue{ - MsgPack: configBytes, - }, - } - - resp, err := server.PlanResourceChange(context.Background(), testReq) - if err != nil { - t.Fatal(err) - } - - plannedStateVal, err := msgpack.Unmarshal(resp.PlannedState.MsgPack, schema.ImpliedType()) - if err != nil { - t.Fatal(err) - } - - if !cmp.Equal(proposedVal, plannedStateVal, valueComparer) { - t.Fatal(cmp.Diff(proposedVal, plannedStateVal, valueComparer)) - } - - plannedStateFoo, acc := plannedStateVal.GetAttr("foo").AsBigFloat().Int64() - if acc != big.Exact { - t.Fatalf("Expected exact accuracy, got %s", acc) - } - if plannedStateFoo != 7227701560655103598 { - t.Fatalf("Expected %d, got %d, this represents a loss of precision in planning large numbers", 7227701560655103598, plannedStateFoo) - } -} - -func TestApplyResourceChange(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - server *GRPCProviderServer - req *tfprotov5.ApplyResourceChangeRequest - expected *tfprotov5.ApplyResourceChangeResponse - }{ - "create: write-only values are nullified in ApplyResourceChangeResponse": { + "deferred-with-provider-plan-modification": { server: NewGRPCProviderServer(&Provider{ + providerDeferred: &Deferred{ + Reason: DeferredReasonProviderConfigUnknown, + }, ResourcesMap: map[string]*Resource{ "test": { + ResourceBehavior: ResourceBehavior{ + ProviderDeferred: ProviderDeferredBehavior{ + // Will ensure that CustomizeDiff is called + EnablePlanModification: true, + }, + }, SchemaVersion: 4, - CreateContext: func(_ context.Context, rd *ResourceData, _ interface{}) diag.Diagnostics { - rd.SetId("baz") - return nil + CustomizeDiff: func(ctx context.Context, d *ResourceDiff, i interface{}) error { + return d.SetNew("foo", "new-foo-value") }, Schema: map[string]*Schema{ "foo": { - Type: TypeString, - Optional: true, - WriteOnly: true, - }, - "bar": { - Type: TypeString, - Optional: true, - WriteOnly: true, + Type: TypeString, + Optional: true, + Computed: true, }, }, }, }, }), - req: &tfprotov5.ApplyResourceChangeRequest{ + req: &tfprotov5.PlanResourceChangeRequest{ TypeName: "test", + ClientCapabilities: &tfprotov5.PlanResourceChangeClientCapabilities{ + DeferralAllowed: true, + }, PriorState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ "foo": cty.String, - "bar": cty.String, }), cty.NullVal( cty.Object(map[string]cty.Type{ "foo": cty.String, - "bar": cty.String, }), ), ), }, - PlannedState: &tfprotov5.DynamicValue{ + ProposedNewState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ "id": cty.String, "foo": cty.String, - "bar": cty.String, }), cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), - "foo": cty.StringVal("baz"), - "bar": cty.StringVal("boop"), + "foo": cty.UnknownVal(cty.String), }), ), }, @@ -6701,115 +6617,1831 @@ func TestApplyResourceChange(t *testing.T) { cty.Object(map[string]cty.Type{ "id": cty.String, "foo": cty.String, - "bar": cty.String, }), cty.ObjectVal(map[string]cty.Value{ "id": cty.NullVal(cty.String), - "foo": cty.StringVal("baz"), - "bar": cty.StringVal("boop"), + "foo": cty.NullVal(cty.String), }), ), }, }, - expected: &tfprotov5.ApplyResourceChangeResponse{ - NewState: &tfprotov5.DynamicValue{ + expected: &tfprotov5.PlanResourceChangeResponse{ + Deferred: &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReasonProviderConfigUnknown, + }, + PlannedState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ "id": cty.String, "foo": cty.String, - "bar": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("baz"), - "foo": cty.NullVal(cty.String), - "bar": cty.NullVal(cty.String), + "id": cty.UnknownVal(cty.String), + "foo": cty.StringVal("new-foo-value"), }), ), }, - Private: []uint8(`{"schema_version":"4"}`), + RequiresReplace: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("id"), + }, + PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), UnsafeToUseLegacyTypeSystem: true, }, }, - "update: write-only values are nullified in ApplyResourceChangeResponse": { + "deferred-skip-plan-modification": { server: NewGRPCProviderServer(&Provider{ + providerDeferred: &Deferred{ + Reason: DeferredReasonProviderConfigUnknown, + }, ResourcesMap: map[string]*Resource{ "test": { SchemaVersion: 4, - CreateContext: func(_ context.Context, rd *ResourceData, _ interface{}) diag.Diagnostics { - rd.SetId("baz") - s := rd.Get("configured").(string) - err := rd.Set("configured", s) - if err != nil { - return nil - } - return nil + CustomizeDiff: func(ctx context.Context, d *ResourceDiff, i interface{}) error { + return errors.New("Test assertion failed: CustomizeDiff shouldn't be called") }, Schema: map[string]*Schema{ - "configured": { + "foo": { Type: TypeString, Optional: true, - }, - "write_onlyA": { - Type: TypeString, - Optional: true, - WriteOnly: true, - }, - "write_onlyB": { - Type: TypeString, - Optional: true, - WriteOnly: true, + Computed: true, }, }, }, }, }), - req: &tfprotov5.ApplyResourceChangeRequest{ + req: &tfprotov5.PlanResourceChangeRequest{ TypeName: "test", + ClientCapabilities: &tfprotov5.PlanResourceChangeClientCapabilities{ + DeferralAllowed: true, + }, PriorState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "configured": cty.String, - "write_onlyA": cty.String, - "write_onlyB": cty.String, + "foo": cty.String, + }), + cty.NullVal( + cty.Object(map[string]cty.Type{ + "foo": cty.String, + }), + ), + ), + }, + ProposedNewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.NullVal(cty.String), - "configured": cty.StringVal("prior_val"), - "write_onlyA": cty.NullVal(cty.String), - "write_onlyB": cty.NullVal(cty.String), + "id": cty.UnknownVal(cty.String), + "foo": cty.StringVal("from-config!"), + }), + ), + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "foo": cty.StringVal("from-config!"), }), ), }, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + Deferred: &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReasonProviderConfigUnknown, + }, + // Returns proposed new state with deferred response PlannedState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "configured": cty.String, - "write_onlyA": cty.String, - "write_onlyB": cty.String, + "id": cty.String, + "foo": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "configured": cty.StringVal("updated_val"), - "write_onlyA": cty.StringVal("foo"), - "write_onlyB": cty.StringVal("bar"), + "id": cty.UnknownVal(cty.String), + "foo": cty.StringVal("from-config!"), }), ), }, + UnsafeToUseLegacyTypeSystem: true, + }, + }, + "create: write-only value can be retrieved in CustomizeDiff": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 4, + CustomizeDiff: func(ctx context.Context, d *ResourceDiff, i interface{}) error { + val := d.Get("foo") + if val != "bar" { + t.Fatalf("Incorrect write-only value") + } + + return nil + }, + Schema: map[string]*Schema{ + "foo": { + Type: TypeString, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }), + req: &tfprotov5.PlanResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "foo": cty.String, + }), + cty.NullVal( + cty.Object(map[string]cty.Type{ + "foo": cty.String, + }), + ), + ), + }, + ProposedNewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.StringVal("bar"), + }), + ), + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "foo": cty.StringVal("bar"), + }), + ), + }, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.NullVal(cty.String), + }), + ), + }, + PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), + RequiresReplace: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("id"), + }, + UnsafeToUseLegacyTypeSystem: true, + }, + }, + "create: write-only values are nullified in PlanResourceChangeResponse": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 4, + Schema: map[string]*Schema{ + "foo": { + Type: TypeString, + Optional: true, + WriteOnly: true, + }, + "bar": { + Type: TypeString, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }), + req: &tfprotov5.PlanResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.String, + }), + cty.NullVal( + cty.Object(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.String, + }), + ), + ), + }, + ProposedNewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.String, + "bar": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.StringVal("baz"), + "bar": cty.StringVal("boop"), + }), + ), + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.String, + "bar": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "foo": cty.StringVal("baz"), + "bar": cty.StringVal("boop"), + }), + ), + }, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.String, + "bar": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.NullVal(cty.String), + "bar": cty.NullVal(cty.String), + }), + ), + }, + PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), + RequiresReplace: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("id"), + }, + UnsafeToUseLegacyTypeSystem: true, + }, + }, + "update: write-only value can be retrieved in CustomizeDiff": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 4, + CustomizeDiff: func(ctx context.Context, d *ResourceDiff, i interface{}) error { + val := d.Get("write_only") + if val != "bar" { + t.Fatalf("Incorrect write-only value") + } + + return nil + }, + Schema: map[string]*Schema{ + "configured": { + Type: TypeString, + Optional: true, + }, + "write_only": { + Type: TypeString, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }), + req: &tfprotov5.PlanResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "configured": cty.String, + "write_only": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "configured": cty.StringVal("prior_val"), + "write_only": cty.NullVal(cty.String), + }), + ), + }, + ProposedNewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "configured": cty.String, + "write_only": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "configured": cty.StringVal("updated_val"), + "write_only": cty.StringVal("bar"), + }), + ), + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "configured": cty.String, + "write_only": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "configured": cty.StringVal("updated_val"), + "write_only": cty.StringVal("bar"), + }), + ), + }, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "configured": cty.String, + "write_only": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "configured": cty.StringVal("updated_val"), + "write_only": cty.NullVal(cty.String), + }), + ), + }, + PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), + RequiresReplace: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("id"), + }, + UnsafeToUseLegacyTypeSystem: true, + }, + }, + "update: write-only values are nullified in PlanResourceChangeResponse": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 4, + Schema: map[string]*Schema{ + "configured": { + Type: TypeString, + Optional: true, + }, + "write_onlyA": { + Type: TypeString, + Optional: true, + WriteOnly: true, + }, + "write_onlyB": { + Type: TypeString, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }), + req: &tfprotov5.PlanResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "configured": cty.String, + "write_onlyA": cty.String, + "write_onlyB": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "configured": cty.StringVal("prior_val"), + "write_onlyA": cty.NullVal(cty.String), + "write_onlyB": cty.NullVal(cty.String), + }), + ), + }, + ProposedNewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "configured": cty.String, + "write_onlyA": cty.String, + "write_onlyB": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "configured": cty.StringVal("updated_val"), + "write_onlyA": cty.StringVal("foo"), + "write_onlyB": cty.StringVal("bar"), + }), + ), + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "configured": cty.String, + "write_onlyA": cty.String, + "write_onlyB": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "configured": cty.StringVal("updated_val"), + "write_onlyA": cty.StringVal("foo"), + "write_onlyB": cty.StringVal("bar"), + }), + ), + }, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "configured": cty.String, + "write_onlyA": cty.String, + "write_onlyB": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "configured": cty.StringVal("updated_val"), + "write_onlyA": cty.NullVal(cty.String), + "write_onlyB": cty.NullVal(cty.String), + }), + ), + }, + PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), + RequiresReplace: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("id"), + }, + UnsafeToUseLegacyTypeSystem: true, + }, + }, + "create-resource-identity-may-change": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 1, + Schema: map[string]*Schema{ + "id": { + Type: TypeString, + Required: true, + }, + "test": { + Type: TypeString, + }, + }, + Identity: &ResourceIdentity{ + Version: 1, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "identity": { + Type: TypeString, + RequiredForImport: true, + }, + } + }, + }, + CustomizeDiff: func(ctx context.Context, d *ResourceDiff, meta interface{}) error { + identity, err := d.Identity() + if err != nil { + return err + } + err = identity.Set("identity", "changed") + if err != nil { + return err + } + return nil + }, + }, + }, + }), + req: &tfprotov5.PlanResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.NullVal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + ), + ), + }, + PriorIdentity: nil, // create! + ProposedNewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "test": cty.StringVal("initial"), + }), + ), + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "test": cty.StringVal("initial"), + }), + ), + }, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "test": cty.StringVal("initial"), + }), + ), + }, + RequiresReplace: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("id"), + }, + PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), + UnsafeToUseLegacyTypeSystem: true, + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "identity": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "identity": cty.StringVal("changed"), + }), + ), + }, + }, + }, + }, + "update-resource-identity-may-not-change": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 1, + Schema: map[string]*Schema{ + "id": { + Type: TypeString, + Required: true, + }, + "test": { + Type: TypeString, + }, + }, + Identity: &ResourceIdentity{ + Version: 1, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "identity": { + Type: TypeString, + RequiredForImport: true, + }, + } + }, + }, + CustomizeDiff: func(ctx context.Context, d *ResourceDiff, meta interface{}) error { + identity, err := d.Identity() + if err != nil { + return err + } + err = identity.Set("identity", "changed") + if err != nil { + return err + } + return nil + }, + }, + }, + }), + req: &tfprotov5.PlanResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("initial"), + "test": cty.StringVal("initial"), + }), + ), + }, + PriorIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "identity": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "identity": cty.StringVal("initial"), + }), + ), + }, + }, + ProposedNewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "test": cty.StringVal("initial"), + }), + ), + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "test": cty.StringVal("initial"), + }), + ), + }, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "test": cty.StringVal("initial"), + }), + ), + }, + RequiresReplace: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("id"), + }, + PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), + UnsafeToUseLegacyTypeSystem: true, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: `Unexpected Identity Change: During the planning operation, the Terraform Provider unexpectedly returned a different identity than the previously stored one. + +This is always a problem with the provider and should be reported to the provider developer. + +Prior Identity: cty.ObjectVal(map[string]cty.Value{"identity":cty.StringVal("initial")}) + +Planned Identity: cty.ObjectVal(map[string]cty.Value{"identity":cty.StringVal("changed")})`, + }, + }, + }, + }, + "update-resource-identity-may-change-if-mutable-identity-allowed": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + ResourceBehavior: ResourceBehavior{ + MutableIdentity: true, + }, + SchemaVersion: 1, + Schema: map[string]*Schema{ + "id": { + Type: TypeString, + Required: true, + }, + "test": { + Type: TypeString, + }, + }, + Identity: &ResourceIdentity{ + Version: 1, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "identity": { + Type: TypeString, + RequiredForImport: true, + }, + } + }, + }, + CustomizeDiff: func(ctx context.Context, d *ResourceDiff, meta interface{}) error { + identity, err := d.Identity() + if err != nil { + return err + } + err = identity.Set("identity", "changed") + if err != nil { + return err + } + return nil + }, + }, + }, + }), + req: &tfprotov5.PlanResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("initial"), + "test": cty.StringVal("initial"), + }), + ), + }, + PriorIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "identity": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "identity": cty.StringVal("initial"), + }), + ), + }, + }, + ProposedNewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "test": cty.StringVal("initial"), + }), + ), + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "test": cty.StringVal("initial"), + }), + ), + }, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "test": cty.StringVal("initial"), + }), + ), + }, + RequiresReplace: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("id"), + }, + PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), + UnsafeToUseLegacyTypeSystem: true, + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "identity": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "identity": cty.StringVal("changed"), + }), + ), + }, + }, + }, + }, + "update-resource-without-prior-identity-identity-may-change": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 1, + Schema: map[string]*Schema{ + "id": { + Type: TypeString, + Required: true, + }, + "test": { + Type: TypeString, + }, + }, + Identity: &ResourceIdentity{ + Version: 1, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "identity": { + Type: TypeString, + RequiredForImport: true, + }, + } + }, + }, + CustomizeDiff: func(ctx context.Context, d *ResourceDiff, meta interface{}) error { + identity, err := d.Identity() + if err != nil { + return err + } + err = identity.Set("identity", "changed") + if err != nil { + return err + } + return nil + }, + }, + }, + }), + req: &tfprotov5.PlanResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("initial"), + "test": cty.StringVal("initial"), + }), + ), + }, + PriorIdentity: nil, // no identity yet (prior provider version didn't support it and there was an upgrade without refresh) + ProposedNewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("initial"), + "test": cty.StringVal("initial"), + }), + ), + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "test": cty.StringVal("initial"), + }), + ), + }, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("initial"), + "test": cty.StringVal("initial"), + }), + ), + }, + PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), + UnsafeToUseLegacyTypeSystem: true, + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "identity": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "identity": cty.StringVal("changed"), + }), + ), + }, + }, + }, + }, + "destroy-resource-identity-may-not-change": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 1, + Schema: map[string]*Schema{ + "id": { + Type: TypeString, + Required: true, + }, + "test": { + Type: TypeString, + }, + }, + Identity: &ResourceIdentity{ + Version: 1, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "identity": { + Type: TypeString, + RequiredForImport: true, + }, + } + }, + }, + CustomizeDiff: func(ctx context.Context, d *ResourceDiff, meta interface{}) error { + identity, err := d.Identity() + if err != nil { + return err + } + // Note: this entire function won't be run anyways for destroys (as that'll short circuit and return the prior identity) + // it's still in this test so we'd see this as an error if something breaks over in the handler + err = identity.Set("identity", "changed_this_should_not_appear_anywhere!") + if err != nil { + return err + } + return nil + }, + }, + }, + }), + req: &tfprotov5.PlanResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("initial"), + "test": cty.StringVal("initial"), + }), + ), + }, + PriorIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "identity": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "identity": cty.StringVal("initial"), + }), + ), + }, + }, + ProposedNewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.NullVal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + ), + ), + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "test": cty.StringVal("initial"), + }), + ), + }, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.NullVal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + ), + ), + }, + UnsafeToUseLegacyTypeSystem: true, + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "identity": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "identity": cty.StringVal("initial"), + }), + ), + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + resp, err := testCase.server.PlanResourceChange(context.Background(), testCase.req) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(resp, testCase.expected, valueComparer); diff != "" { + ty := testCase.server.getResourceSchemaBlock("test").ImpliedType() + + if resp != nil && resp.PlannedState != nil { + t.Logf("resp.PlannedState.MsgPack: %s", mustMsgpackUnmarshal(ty, resp.PlannedState.MsgPack)) + } + + if testCase.expected != nil && testCase.expected.PlannedState != nil { + t.Logf("expected: %s", mustMsgpackUnmarshal(ty, testCase.expected.PlannedState.MsgPack)) + } + + t.Error(diff) + } + }) + } +} + +func TestPlanResourceChange_bigint(t *testing.T) { + r := &Resource{ + UseJSONNumber: true, + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + Required: true, + }, + }, + } + + server := NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": r, + }, + }) + + schema := r.CoreConfigSchema() + priorState, err := msgpack.Marshal(cty.NullVal(schema.ImpliedType()), schema.ImpliedType()) + if err != nil { + t.Fatal(err) + } + + proposedVal := cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.MustParseNumberVal("7227701560655103598"), + }) + proposedState, err := msgpack.Marshal(proposedVal, schema.ImpliedType()) + if err != nil { + t.Fatal(err) + } + + config, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "foo": cty.MustParseNumberVal("7227701560655103598"), + })) + if err != nil { + t.Fatal(err) + } + configBytes, err := msgpack.Marshal(config, schema.ImpliedType()) + if err != nil { + t.Fatal(err) + } + + testReq := &tfprotov5.PlanResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: priorState, + }, + ProposedNewState: &tfprotov5.DynamicValue{ + MsgPack: proposedState, + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: configBytes, + }, + } + + resp, err := server.PlanResourceChange(context.Background(), testReq) + if err != nil { + t.Fatal(err) + } + + plannedStateVal, err := msgpack.Unmarshal(resp.PlannedState.MsgPack, schema.ImpliedType()) + if err != nil { + t.Fatal(err) + } + + if !cmp.Equal(proposedVal, plannedStateVal, valueComparer) { + t.Fatal(cmp.Diff(proposedVal, plannedStateVal, valueComparer)) + } + + plannedStateFoo, acc := plannedStateVal.GetAttr("foo").AsBigFloat().Int64() + if acc != big.Exact { + t.Fatalf("Expected exact accuracy, got %s", acc) + } + if plannedStateFoo != 7227701560655103598 { + t.Fatalf("Expected %d, got %d, this represents a loss of precision in planning large numbers", 7227701560655103598, plannedStateFoo) + } +} + +func TestApplyResourceChange(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + server *GRPCProviderServer + req *tfprotov5.ApplyResourceChangeRequest + expected *tfprotov5.ApplyResourceChangeResponse + }{ + "create: write-only values are nullified in ApplyResourceChangeResponse": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 4, + CreateContext: func(_ context.Context, rd *ResourceData, _ interface{}) diag.Diagnostics { + rd.SetId("baz") + return nil + }, + Schema: map[string]*Schema{ + "foo": { + Type: TypeString, + Optional: true, + WriteOnly: true, + }, + "bar": { + Type: TypeString, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }), + req: &tfprotov5.ApplyResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.String, + }), + cty.NullVal( + cty.Object(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.String, + }), + ), + ), + }, + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.String, + "bar": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.StringVal("baz"), + "bar": cty.StringVal("boop"), + }), + ), + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.String, + "bar": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "foo": cty.StringVal("baz"), + "bar": cty.StringVal("boop"), + }), + ), + }, + }, + expected: &tfprotov5.ApplyResourceChangeResponse{ + NewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.String, + "bar": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("baz"), + "foo": cty.NullVal(cty.String), + "bar": cty.NullVal(cty.String), + }), + ), + }, + Private: []uint8(`{"schema_version":"4"}`), + UnsafeToUseLegacyTypeSystem: true, + }, + }, + "update: write-only values are nullified in ApplyResourceChangeResponse": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 4, + CreateContext: func(_ context.Context, rd *ResourceData, _ interface{}) diag.Diagnostics { + rd.SetId("baz") + s := rd.Get("configured").(string) + err := rd.Set("configured", s) + if err != nil { + return nil + } + return nil + }, + Schema: map[string]*Schema{ + "configured": { + Type: TypeString, + Optional: true, + }, + "write_onlyA": { + Type: TypeString, + Optional: true, + WriteOnly: true, + }, + "write_onlyB": { + Type: TypeString, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }), + req: &tfprotov5.ApplyResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "configured": cty.String, + "write_onlyA": cty.String, + "write_onlyB": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "configured": cty.StringVal("prior_val"), + "write_onlyA": cty.NullVal(cty.String), + "write_onlyB": cty.NullVal(cty.String), + }), + ), + }, + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "configured": cty.String, + "write_onlyA": cty.String, + "write_onlyB": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "configured": cty.StringVal("updated_val"), + "write_onlyA": cty.StringVal("foo"), + "write_onlyB": cty.StringVal("bar"), + }), + ), + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "configured": cty.String, + "write_onlyA": cty.String, + "write_onlyB": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "configured": cty.StringVal("updated_val"), + "write_onlyA": cty.StringVal("foo"), + "write_onlyB": cty.StringVal("bar"), + }), + ), + }, + }, + expected: &tfprotov5.ApplyResourceChangeResponse{ + NewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "configured": cty.String, + "write_onlyA": cty.String, + "write_onlyB": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("baz"), + "configured": cty.StringVal("updated_val"), + "write_onlyA": cty.NullVal(cty.String), + "write_onlyB": cty.NullVal(cty.String), + }), + ), + }, + Private: []uint8(`{"schema_version":"4"}`), + UnsafeToUseLegacyTypeSystem: true, + }, + }, + "create: identity returned in ApplyResourceChangeResponse": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 4, + CreateContext: func(_ context.Context, rd *ResourceData, _ interface{}) diag.Diagnostics { + rd.SetId("baz") + identity, err := rd.Identity() + if err != nil { + t.Fatal(err) + } + err = identity.Set("ident", "bazz") + if err != nil { + t.Fatal(err) + } + return nil + }, + Schema: map[string]*Schema{}, + Identity: &ResourceIdentity{ + Version: 1, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "ident": { + Type: TypeString, + RequiredForImport: true, + }, + } + }, + }, + }, + }, + }), + req: &tfprotov5.ApplyResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{}), + cty.NullVal( + cty.Object(map[string]cty.Type{}), + ), + ), + }, + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + }), + ), + }, + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "ident": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "ident": cty.NullVal(cty.String), + }), + ), + }, + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + }), + ), + }, + }, + expected: &tfprotov5.ApplyResourceChangeResponse{ + NewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("baz"), + }), + ), + }, + Private: []uint8(`{"schema_version":"4"}`), + UnsafeToUseLegacyTypeSystem: true, + NewIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "ident": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "ident": cty.StringVal("bazz"), + }), + ), + }, + }, + }, + }, + "create: no identity schema diag in ApplyResourceChangeResponse": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 4, + Schema: map[string]*Schema{}, + Identity: &ResourceIdentity{ + Version: 1, + }, + }, + }, + }), + req: &tfprotov5.ApplyResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{}), + cty.NullVal( + cty.Object(map[string]cty.Type{}), + ), + ), + }, + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + }), + ), + }, + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "ident": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "ident": cty.UnknownVal(cty.String), + }), + ), + }, + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + }), + ), + }, + }, + expected: &tfprotov5.ApplyResourceChangeResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "getting identity schema failed for resource 'test': resource does not have an identity schema", + }, + }, + NewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal(cty.DynamicPseudoType, cty.NullVal(cty.DynamicPseudoType)), + }, + }, + }, + "create: empty identity schema diag in ApplyResourceChangeResponse": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 4, + Schema: map[string]*Schema{}, + Identity: &ResourceIdentity{ + Version: 1, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{} + }, + }, + }, + }, + }), + req: &tfprotov5.ApplyResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{}), + cty.NullVal( + cty.Object(map[string]cty.Type{}), + ), + ), + }, + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + }), + ), + }, + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "ident": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "ident": cty.UnknownVal(cty.String), + }), + ), + }, + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + }), + ), + }, + }, + expected: &tfprotov5.ApplyResourceChangeResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "getting identity schema failed for resource 'test': identity schema must have at least one attribute", + }, + }, + NewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal(cty.DynamicPseudoType, cty.NullVal(cty.DynamicPseudoType)), + }, + }, + }, + "create-resource-identity-may-change": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 1, + Schema: map[string]*Schema{ + "id": { + Type: TypeString, + Required: true, + }, + "test": { + Type: TypeString, + }, + }, + Identity: &ResourceIdentity{ + Version: 1, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "identity": { + Type: TypeString, + RequiredForImport: true, + }, + } + }, + }, + CreateContext: func(_ context.Context, rd *ResourceData, _ interface{}) diag.Diagnostics { + identity, err := rd.Identity() + if err != nil { + return diag.FromErr(err) + } + err = identity.Set("identity", "changed") + if err != nil { + return diag.FromErr(err) + } + rd.SetId("changed") + return nil + }, + }, + }, + }), + req: &tfprotov5.ApplyResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.NullVal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + ), + ), + }, + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "test": cty.StringVal("initial"), + }), + ), + }, + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "identity": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "identity": cty.StringVal("initial"), + }), + ), + }, + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "test": cty.StringVal("initial"), + }), + ), + }, + }, + expected: &tfprotov5.ApplyResourceChangeResponse{ + NewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("changed"), + "test": cty.StringVal("initial"), + }), + ), + }, + Private: []uint8(`{"schema_version":"1"}`), + UnsafeToUseLegacyTypeSystem: true, + NewIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "identity": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "identity": cty.StringVal("changed"), + }), + ), + }, + }, + }, + }, + "update-resource-identity-may-not-change": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 1, + Schema: map[string]*Schema{ + "id": { + Type: TypeString, + Required: true, + }, + "test": { + Type: TypeString, + }, + }, + Identity: &ResourceIdentity{ + Version: 1, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "identity": { + Type: TypeString, + RequiredForImport: true, + }, + } + }, + }, + UpdateContext: func(_ context.Context, rd *ResourceData, _ interface{}) diag.Diagnostics { + identity, err := rd.Identity() + if err != nil { + return diag.FromErr(err) + } + err = identity.Set("identity", "changed") + if err != nil { + return diag.FromErr(err) + } + rd.SetId("changed") + return nil + }, + }, + }, + }), + req: &tfprotov5.ApplyResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("initial"), + "test": cty.StringVal("initial"), + }), + ), + }, + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("initial"), + "test": cty.StringVal("initial"), + }), + ), + }, + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "identity": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "identity": cty.StringVal("initial"), + }), + ), + }, + }, Config: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "configured": cty.String, - "write_onlyA": cty.String, - "write_onlyB": cty.String, + "id": cty.String, + "test": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.NullVal(cty.String), - "configured": cty.StringVal("updated_val"), - "write_onlyA": cty.StringVal("foo"), - "write_onlyB": cty.StringVal("bar"), + "id": cty.NullVal(cty.String), + "test": cty.StringVal("initial"), }), ), }, @@ -6818,52 +8450,70 @@ func TestApplyResourceChange(t *testing.T) { NewState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - "configured": cty.String, - "write_onlyA": cty.String, - "write_onlyB": cty.String, + "id": cty.String, + "test": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("baz"), - "configured": cty.StringVal("updated_val"), - "write_onlyA": cty.NullVal(cty.String), - "write_onlyB": cty.NullVal(cty.String), + "id": cty.StringVal("changed"), + "test": cty.StringVal("initial"), }), ), }, - Private: []uint8(`{"schema_version":"4"}`), - UnsafeToUseLegacyTypeSystem: true, + Private: []uint8(`{"schema_version":"1"}`), + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: `Unexpected Identity Change: During the update operation, the Terraform Provider unexpectedly returned a different identity than the previously stored one. + +This is always a problem with the provider and should be reported to the provider developer. + +Planned Identity: cty.ObjectVal(map[string]cty.Value{"identity":cty.StringVal("initial")}) + +New Identity: cty.ObjectVal(map[string]cty.Value{"identity":cty.StringVal("changed")})`, + }, + }, }, }, - "create: identity returned in ApplyResourceChangeResponse": { + "update-resource-identity-may-change-if-mutable-identity-allowed": { server: NewGRPCProviderServer(&Provider{ ResourcesMap: map[string]*Resource{ "test": { - SchemaVersion: 4, - CreateContext: func(_ context.Context, rd *ResourceData, _ interface{}) diag.Diagnostics { - rd.SetId("baz") - identity, err := rd.Identity() - if err != nil { - t.Fatal(err) - } - err = identity.Set("ident", "bazz") - if err != nil { - t.Fatal(err) - } - return nil + ResourceBehavior: ResourceBehavior{ + MutableIdentity: true, + }, + SchemaVersion: 1, + Schema: map[string]*Schema{ + "id": { + Type: TypeString, + Required: true, + }, + "test": { + Type: TypeString, + }, }, - Schema: map[string]*Schema{}, Identity: &ResourceIdentity{ Version: 1, SchemaFunc: func() map[string]*Schema { return map[string]*Schema{ - "ident": { + "identity": { Type: TypeString, RequiredForImport: true, }, } }, }, + UpdateContext: func(_ context.Context, rd *ResourceData, _ interface{}) diag.Diagnostics { + identity, err := rd.Identity() + if err != nil { + return diag.FromErr(err) + } + err = identity.Set("identity", "changed") + if err != nil { + return diag.FromErr(err) + } + rd.SetId("changed") + return nil + }, }, }, }), @@ -6871,19 +8521,25 @@ func TestApplyResourceChange(t *testing.T) { TypeName: "test", PriorState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( - cty.Object(map[string]cty.Type{}), - cty.NullVal( - cty.Object(map[string]cty.Type{}), - ), + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("initial"), + "test": cty.StringVal("initial"), + }), ), }, PlannedState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, + "id": cty.String, + "test": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), + "id": cty.StringVal("initial"), + "test": cty.StringVal("initial"), }), ), }, @@ -6891,10 +8547,10 @@ func TestApplyResourceChange(t *testing.T) { IdentityData: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "ident": cty.String, + "identity": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "ident": cty.UnknownVal(cty.String), + "identity": cty.StringVal("initial"), }), ), }, @@ -6902,10 +8558,12 @@ func TestApplyResourceChange(t *testing.T) { Config: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, + "id": cty.String, + "test": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.NullVal(cty.String), + "id": cty.NullVal(cty.String), + "test": cty.StringVal("initial"), }), ), }, @@ -6914,37 +8572,67 @@ func TestApplyResourceChange(t *testing.T) { NewState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, + "id": cty.String, + "test": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("baz"), + "id": cty.StringVal("changed"), + "test": cty.StringVal("initial"), }), ), }, - Private: []uint8(`{"schema_version":"4"}`), + Private: []uint8(`{"schema_version":"1"}`), UnsafeToUseLegacyTypeSystem: true, NewIdentity: &tfprotov5.ResourceIdentityData{ IdentityData: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "ident": cty.String, + "identity": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "ident": cty.StringVal("bazz"), + "identity": cty.StringVal("changed"), }), ), }, }, }, }, - "create: no identity schema diag in ApplyResourceChangeResponse": { + "update-resource-without-planned-identity-identity-may-change": { server: NewGRPCProviderServer(&Provider{ ResourcesMap: map[string]*Resource{ "test": { - SchemaVersion: 4, - Schema: map[string]*Schema{}, + SchemaVersion: 1, + Schema: map[string]*Schema{ + "id": { + Type: TypeString, + Required: true, + }, + "test": { + Type: TypeString, + }, + }, Identity: &ResourceIdentity{ Version: 1, + SchemaFunc: func() map[string]*Schema { + return map[string]*Schema{ + "identity": { + Type: TypeString, + RequiredForImport: true, + }, + } + }, + }, + UpdateContext: func(_ context.Context, rd *ResourceData, _ interface{}) diag.Diagnostics { + identity, err := rd.Identity() + if err != nil { + return diag.FromErr(err) + } + err = identity.Set("identity", "changed") + if err != nil { + return diag.FromErr(err) + } + rd.SetId("changed") + return nil }, }, }, @@ -6953,69 +8641,108 @@ func TestApplyResourceChange(t *testing.T) { TypeName: "test", PriorState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( - cty.Object(map[string]cty.Type{}), - cty.NullVal( - cty.Object(map[string]cty.Type{}), - ), + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("initial"), + "test": cty.StringVal("initial"), + }), ), }, PlannedState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, + "id": cty.String, + "test": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), + "id": cty.StringVal("initial"), + "test": cty.StringVal("initial"), }), ), }, - PlannedIdentity: &tfprotov5.ResourceIdentityData{ - IdentityData: &tfprotov5.DynamicValue{ - MsgPack: mustMsgpackMarshal( - cty.Object(map[string]cty.Type{ - "ident": cty.String, - }), - cty.ObjectVal(map[string]cty.Value{ - "ident": cty.UnknownVal(cty.String), - }), - ), - }, - }, + PlannedIdentity: nil, Config: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, + "id": cty.String, + "test": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.NullVal(cty.String), + "id": cty.NullVal(cty.String), + "test": cty.StringVal("initial"), }), ), }, }, expected: &tfprotov5.ApplyResourceChangeResponse{ - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "getting identity schema failed for resource 'test': resource does not have an identity schema", - }, - }, NewState: &tfprotov5.DynamicValue{ - MsgPack: mustMsgpackMarshal(cty.DynamicPseudoType, cty.NullVal(cty.DynamicPseudoType)), + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("changed"), + "test": cty.StringVal("initial"), + }), + ), + }, + Private: []uint8(`{"schema_version":"1"}`), + UnsafeToUseLegacyTypeSystem: true, + NewIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "identity": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "identity": cty.StringVal("changed"), + }), + ), + }, }, }, }, - "create: empty identity schema diag in ApplyResourceChangeResponse": { + "destroy-resource-identity-may-change": { server: NewGRPCProviderServer(&Provider{ ResourcesMap: map[string]*Resource{ "test": { - SchemaVersion: 4, - Schema: map[string]*Schema{}, + SchemaVersion: 1, + Schema: map[string]*Schema{ + "id": { + Type: TypeString, + Required: true, + }, + "test": { + Type: TypeString, + }, + }, Identity: &ResourceIdentity{ Version: 1, SchemaFunc: func() map[string]*Schema { - return map[string]*Schema{} + return map[string]*Schema{ + "identity": { + Type: TypeString, + RequiredForImport: true, + }, + } }, }, + DeleteContext: func(_ context.Context, rd *ResourceData, _ interface{}) diag.Diagnostics { + identity, err := rd.Identity() + if err != nil { + return diag.FromErr(err) + } + err = identity.Set("identity", "changed") + if err != nil { + return diag.FromErr(err) + } + rd.SetId("changed") + return nil + }, }, }, }), @@ -7023,30 +8750,36 @@ func TestApplyResourceChange(t *testing.T) { TypeName: "test", PriorState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( - cty.Object(map[string]cty.Type{}), - cty.NullVal( - cty.Object(map[string]cty.Type{}), - ), + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("initial"), + "test": cty.StringVal("initial"), + }), ), }, PlannedState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, - }), - cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), + "id": cty.String, + "test": cty.String, }), + cty.NullVal(cty.Object(map[string]cty.Type{ // NullVal => destroy + "id": cty.String, + "test": cty.String, + })), ), }, PlannedIdentity: &tfprotov5.ResourceIdentityData{ IdentityData: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "ident": cty.String, + "identity": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "ident": cty.UnknownVal(cty.String), + "identity": cty.StringVal("initial"), }), ), }, @@ -7054,23 +8787,28 @@ func TestApplyResourceChange(t *testing.T) { Config: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "id": cty.String, + "id": cty.String, + "test": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "id": cty.NullVal(cty.String), + "id": cty.NullVal(cty.String), + "test": cty.StringVal("initial"), }), ), }, }, expected: &tfprotov5.ApplyResourceChangeResponse{ - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "getting identity schema failed for resource 'test': identity schema must have at least one attribute", - }, - }, NewState: &tfprotov5.DynamicValue{ - MsgPack: mustMsgpackMarshal(cty.DynamicPseudoType, cty.NullVal(cty.DynamicPseudoType)), + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + }), + cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + })), + ), }, }, }, @@ -7710,7 +9448,7 @@ func TestImportResourceState(t *testing.T) { }), ), }, - Private: []byte(`{"schema_version":"1"}`), + Private: []byte(`{".import_before_read":true,"schema_version":"1"}`), }, }, }, @@ -7796,7 +9534,7 @@ func TestImportResourceState(t *testing.T) { }), ), }, - Private: []byte(`{"schema_version":"1"}`), + Private: []byte(`{".import_before_read":true,"schema_version":"1"}`), Identity: &tfprotov5.ResourceIdentityData{ IdentityData: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( @@ -8064,7 +9802,7 @@ func TestImportResourceState(t *testing.T) { }), ), }, - Private: []byte(`{"schema_version":"1"}`), + Private: []byte(`{".import_before_read":true,"schema_version":"1"}`), }, }, }, diff --git a/helper/schema/resource.go b/helper/schema/resource.go index 0bf7f9597d0..83140436800 100644 --- a/helper/schema/resource.go +++ b/helper/schema/resource.go @@ -674,6 +674,11 @@ type ResourceBehavior struct { // NOTE: This functionality is related to deferred action support, which is currently experimental and is subject // to change or break without warning. It is not protected by version compatibility guarantees. ProviderDeferred ProviderDeferredBehavior + + // MutableIdentity indicates that the managed resource supports an identity that can change during the + // resource's lifecycle. Setting this flag to true will disable the SDK validation that ensures identity + // data doesn't change during RPC calls. + MutableIdentity bool } // ProviderDeferredBehavior enables provider-defined logic to be executed diff --git a/terraform/state.go b/terraform/state.go index 2004b65dc61..f905c2a93d2 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -30,6 +30,13 @@ const ( stateVersion = 3 ) +// ImportBeforeReadMetaKey is an internal private field used to indicate that the current resource state and identity +// were provided most recently by the ImportResourceState RPC. This indicates that the state is an import stub and identity +// has not been stored in state yet. +// +// When detected, this key should be cleared before returning from the ReadResource RPC. +var ImportBeforeReadMetaKey = ".import_before_read" + // rootModulePath is the path of the root module var rootModulePath = []string{"root"} From b3361de234495260d9fd8a0588fc557193484f85 Mon Sep 17 00:00:00 2001 From: Ansgar Mertens Date: Thu, 15 May 2025 13:13:53 +0200 Subject: [PATCH 47/53] github: Use Dependabot to keep Actions updated (#1481) --- .github/dependabot.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 552d6ab99c4..2b70bd48e5e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -14,10 +14,7 @@ updates: directory: "/tools" schedule: interval: "daily" - # Dependabot only updates hashicorp GHAs, external GHAs are managed by internal tooling (tsccr) - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" - allow: - - dependency-name: "hashicorp/*" From 39397e17d5f382133967d5e8b78e75b0e525a7e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 May 2025 14:28:26 -0400 Subject: [PATCH 48/53] build(deps): bump golangci/golangci-lint-action from 7.0.0 to 8.0.0 (#1484) Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 7.0.0 to 8.0.0. - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/1481404843c368bc19ca9406f87d6e0fc97bdcfd...4afd733a84b1f43292c63897423277bb7f4313a9) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-version: 8.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index 4746969cbaa..4f708f9b408 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -21,7 +21,7 @@ jobs: with: go-version-file: 'go.mod' - run: go mod download - - uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0 + - uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 with: version: latest terraform-provider-corner-tfprotov5: From 67a5b0a9fa47cd693afbe6bfae1822d9fb79d841 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 08:42:37 -0400 Subject: [PATCH 49/53] build(deps): bump actions/setup-go from 5.4.0 to 5.5.0 (#1483) Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5.4.0 to 5.5.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/0aaccfd150d50ccaeb58ebd88d36e91967a5f35b...d35c59abb061a4a6fb18e82ac0862c26744d6ab5) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: 5.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-github-actions.yml | 2 +- .github/workflows/ci-go.yml | 6 +++--- .github/workflows/ci-goreleaser.yml | 2 +- .github/workflows/release.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-github-actions.yml b/.github/workflows/ci-github-actions.yml index 0773b8bc95b..ebdf8a6b857 100644 --- a/.github/workflows/ci-github-actions.yml +++ b/.github/workflows/ci-github-actions.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version-file: 'go.mod' - run: go install github.com/rhysd/actionlint/cmd/actionlint@latest diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index 4f708f9b408..59f87c72049 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version-file: 'go.mod' - run: go mod download @@ -36,7 +36,7 @@ jobs: with: path: terraform-provider-corner repository: hashicorp/terraform-provider-corner - - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version-file: 'go.mod' - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 @@ -60,7 +60,7 @@ jobs: go-version: [ '1.24', '1.23' ] steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: ${{ matrix.go-version }} - run: go mod download diff --git a/.github/workflows/ci-goreleaser.yml b/.github/workflows/ci-goreleaser.yml index 59a7b3d8f30..8750d649e93 100644 --- a/.github/workflows/ci-goreleaser.yml +++ b/.github/workflows/ci-goreleaser.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version-file: 'go.mod' - uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7860f8587ed..457d5367c7a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -134,7 +134,7 @@ jobs: ref: ${{ inputs.versionNumber }} fetch-depth: 0 - - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version-file: 'go.mod' From d547ff7f146190b80be55ee94f76dc18f7dae3f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 08:45:22 -0400 Subject: [PATCH 50/53] build(deps): bump github.com/hashicorp/terraform-json (#1482) Bumps [github.com/hashicorp/terraform-json](https://github.com/hashicorp/terraform-json) from 0.24.0 to 0.25.0. - [Release notes](https://github.com/hashicorp/terraform-json/releases) - [Commits](https://github.com/hashicorp/terraform-json/compare/v0.24.0...v0.25.0) --- updated-dependencies: - dependency-name: github.com/hashicorp/terraform-json dependency-version: 0.25.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d62c985aac4..5a31398dcf6 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/hashicorp/hcl/v2 v2.23.0 github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-exec v0.23.0 - github.com/hashicorp/terraform-json v0.24.0 + github.com/hashicorp/terraform-json v0.25.0 github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/mitchellh/copystructure v1.2.0 diff --git a/go.sum b/go.sum index effb3e6c32f..42d1d8c8ff3 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,8 @@ github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/terraform-exec v0.23.0 h1:MUiBM1s0CNlRFsCLJuM5wXZrzA3MnPYEsiXmzATMW/I= github.com/hashicorp/terraform-exec v0.23.0/go.mod h1:mA+qnx1R8eePycfwKkCRk3Wy65mwInvlpAeOwmA7vlY= -github.com/hashicorp/terraform-json v0.24.0 h1:rUiyF+x1kYawXeRth6fKFm/MdfBS6+lW4NbeATsYz8Q= -github.com/hashicorp/terraform-json v0.24.0/go.mod h1:Nfj5ubo9xbu9uiAoZVBsNOjvNKB66Oyrvtit74kC7ow= +github.com/hashicorp/terraform-json v0.25.0 h1:rmNqc/CIfcWawGiwXmRuiXJKEiJu1ntGoxseG1hLhoQ= +github.com/hashicorp/terraform-json v0.25.0/go.mod h1:sMKS8fiRDX4rVlR6EJUMudg1WcanxCMoWwTLkgZP/vc= github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1 h1:/IZFNUEafGnJGXRe2iNQQ+vtzEw/5qiD+gOxkFrNbi4= github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1/go.mod h1:Tf2HngbyKvovAlGXgBOVGm3EDvbNaN/StUaTXwrej4o= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= From 5ffe9fbfe979bafa815050973189185d1afb8e13 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 14:49:55 -0400 Subject: [PATCH 51/53] build(deps): bump github.com/hashicorp/terraform-plugin-go (#1486) Bumps [github.com/hashicorp/terraform-plugin-go](https://github.com/hashicorp/terraform-plugin-go) from 0.27.0-alpha.1 to 0.27.0. - [Release notes](https://github.com/hashicorp/terraform-plugin-go/releases) - [Changelog](https://github.com/hashicorp/terraform-plugin-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/terraform-plugin-go/compare/v0.27.0-alpha.1...v0.27.0) --- updated-dependencies: - dependency-name: github.com/hashicorp/terraform-plugin-go dependency-version: 0.27.0 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 12 ++++++------ go.sum | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 5a31398dcf6..775f324f8cd 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-exec v0.23.0 github.com/hashicorp/terraform-json v0.25.0 - github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1 + github.com/hashicorp/terraform-plugin-go v0.27.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/mitchellh/copystructure v1.2.0 github.com/mitchellh/go-testing-interface v1.14.1 @@ -38,7 +38,7 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect - github.com/hashicorp/terraform-registry-address v0.2.4 // indirect + github.com/hashicorp/terraform-registry-address v0.2.5 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -49,13 +49,13 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.38.0 // indirect + golang.org/x/net v0.39.0 // indirect golang.org/x/sync v0.14.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect - google.golang.org/grpc v1.71.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/grpc v1.72.1 // indirect + google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/go.sum b/go.sum index 42d1d8c8ff3..1eb6e673606 100644 --- a/go.sum +++ b/go.sum @@ -80,12 +80,12 @@ github.com/hashicorp/terraform-exec v0.23.0 h1:MUiBM1s0CNlRFsCLJuM5wXZrzA3MnPYEs github.com/hashicorp/terraform-exec v0.23.0/go.mod h1:mA+qnx1R8eePycfwKkCRk3Wy65mwInvlpAeOwmA7vlY= github.com/hashicorp/terraform-json v0.25.0 h1:rmNqc/CIfcWawGiwXmRuiXJKEiJu1ntGoxseG1hLhoQ= github.com/hashicorp/terraform-json v0.25.0/go.mod h1:sMKS8fiRDX4rVlR6EJUMudg1WcanxCMoWwTLkgZP/vc= -github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1 h1:/IZFNUEafGnJGXRe2iNQQ+vtzEw/5qiD+gOxkFrNbi4= -github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1/go.mod h1:Tf2HngbyKvovAlGXgBOVGm3EDvbNaN/StUaTXwrej4o= +github.com/hashicorp/terraform-plugin-go v0.27.0 h1:ujykws/fWIdsi6oTUT5Or4ukvEan4aN9lY+LOxVP8EE= +github.com/hashicorp/terraform-plugin-go v0.27.0/go.mod h1:FDa2Bb3uumkTGSkTFpWSOwWJDwA7bf3vdP3ltLDTH6o= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= -github.com/hashicorp/terraform-registry-address v0.2.4 h1:JXu/zHB2Ymg/TGVCRu10XqNa4Sh2bWcqCNyKWjnCPJA= -github.com/hashicorp/terraform-registry-address v0.2.4/go.mod h1:tUNYTVyCtU4OIGXXMDp7WNcJ+0W1B4nmstVDgHMjfAU= +github.com/hashicorp/terraform-registry-address v0.2.5 h1:2GTftHqmUhVOeuu9CW3kwDkRe4pcBDq0uuK5VJngU1M= +github.com/hashicorp/terraform-registry-address v0.2.5/go.mod h1:PpzXWINwB5kuVS5CA7m1+eO2f1jKb5ZDIxrOPfpnGkg= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= @@ -171,8 +171,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -212,14 +212,14 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= +google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From d644d7dd8755dca1c6caad9029029228464a10b9 Mon Sep 17 00:00:00 2001 From: hc-github-team-tf-provider-devex Date: Fri, 16 May 2025 18:52:27 +0000 Subject: [PATCH 52/53] Update meta package SDKVersion --- meta/meta.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meta/meta.go b/meta/meta.go index 9555d134c93..1ba16498736 100644 --- a/meta/meta.go +++ b/meta/meta.go @@ -25,7 +25,7 @@ var SDKVersion = "2.37.0" // // Deprecated: Use Go standard library [runtime/debug] package build information // instead. -var SDKPrerelease = "beta.1" +var SDKPrerelease = "" // SemVer is an instance of version.Version. This has the secondary // benefit of verifying during tests and init time that our version is a From d636e58176355dc17e4f723207206202288daa13 Mon Sep 17 00:00:00 2001 From: hc-github-team-tf-provider-devex Date: Fri, 16 May 2025 18:52:41 +0000 Subject: [PATCH 53/53] Update changelog --- .changes/2.37.0.md | 22 +++++++++++++++++++ .../unreleased/BUG FIXES-20250424-133143.yaml | 5 ----- .../ENHANCEMENTS-20250512-171131.yaml | 6 ----- .../ENHANCEMENTS-20250512-171419.yaml | 5 ----- .../unreleased/FEATURES-20250512-170956.yaml | 5 ----- .../unreleased/FEATURES-20250512-171620.yaml | 5 ----- .../unreleased/FEATURES-20250512-172448.yaml | 5 ----- .../unreleased/NOTES-20250317-152749.yaml | 7 ------ .../unreleased/NOTES-20250512-171756.yaml | 12 ---------- CHANGELOG.md | 22 +++++++++++++++++++ 10 files changed, 44 insertions(+), 50 deletions(-) create mode 100644 .changes/2.37.0.md delete mode 100644 .changes/unreleased/BUG FIXES-20250424-133143.yaml delete mode 100644 .changes/unreleased/ENHANCEMENTS-20250512-171131.yaml delete mode 100644 .changes/unreleased/ENHANCEMENTS-20250512-171419.yaml delete mode 100644 .changes/unreleased/FEATURES-20250512-170956.yaml delete mode 100644 .changes/unreleased/FEATURES-20250512-171620.yaml delete mode 100644 .changes/unreleased/FEATURES-20250512-172448.yaml delete mode 100644 .changes/unreleased/NOTES-20250317-152749.yaml delete mode 100644 .changes/unreleased/NOTES-20250512-171756.yaml diff --git a/.changes/2.37.0.md b/.changes/2.37.0.md new file mode 100644 index 00000000000..f08366086de --- /dev/null +++ b/.changes/2.37.0.md @@ -0,0 +1,22 @@ +## 2.37.0 (May 16, 2025) + +NOTES: + +* all: This Go module has been updated to Go 1.23 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.23 release notes](https://go.dev/doc/go1.23) before upgrading. Any consumers building on earlier Go versions may experience errors. ([#1445](https://github.com/hashicorp/terraform-plugin-sdk/issues/1445)) +* all: This release contains new fields and structs for implmenting managed resource identity. Resource identity is data that is defined by a separate schema and is stored alongside resource state. Identity data is used by Terrform to uniquely identify a remote object and is meant to be immutable during the remote object's lifecycle. Resources that support identity can now be imported using the `identity` attribute in Terraform configuration `import` blocks, available in Terraform v1.12+. The `resource.Identity` field on the `schema.Resource` struct can be used to support identity by defining an identity schema. Once the identity schema is defined, you can read and store identity data in the state file with the new `IdentityData` struct that is available via the `Identity()` method on `schema.ResourceData` and `schema.ResourceDiff` structs. ([#1444](https://github.com/hashicorp/terraform-plugin-sdk/issues/1444)) + +FEATURES: + +* helper/schema: Added new `TestResourceDataWithIdentityRaw` function for creating a `ResourceData` struct with identity data for unit testing. ([#1475](https://github.com/hashicorp/terraform-plugin-sdk/issues/1475)) +* helper/schema: Added new `Identity` field to `Resource` that supports defining an identity schema for managed resources only. ([#1444](https://github.com/hashicorp/terraform-plugin-sdk/issues/1444)) +* Added new `ImportStatePassthroughWithIdentity` helper that can support both identity and ID importing via a single field. ([#1474](https://github.com/hashicorp/terraform-plugin-sdk/issues/1474)) + +ENHANCEMENTS: + +* helper/schema: Added `RequiredForImport` and `OptionalForImport` fields to the `Schema` struct, which are only valid for identity schemas. ([#1444](https://github.com/hashicorp/terraform-plugin-sdk/issues/1444)) +* helper/schema: Updated `ResourceData` to support passing of identity data in CRUD and import functions for managed resources. ([#1444](https://github.com/hashicorp/terraform-plugin-sdk/issues/1444)) + +BUG FIXES: + +* helper/schema: Fixed bug that blocked write-only attributes from being used with resources without update functions. ([#1472](https://github.com/hashicorp/terraform-plugin-sdk/issues/1472)) + diff --git a/.changes/unreleased/BUG FIXES-20250424-133143.yaml b/.changes/unreleased/BUG FIXES-20250424-133143.yaml deleted file mode 100644 index c98ec929bcc..00000000000 --- a/.changes/unreleased/BUG FIXES-20250424-133143.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: BUG FIXES -body: 'helper/schema: Fixed bug that blocked write-only attributes from being used with resources without update functions.' -time: 2025-04-24T13:31:43.847209-04:00 -custom: - Issue: "1472" diff --git a/.changes/unreleased/ENHANCEMENTS-20250512-171131.yaml b/.changes/unreleased/ENHANCEMENTS-20250512-171131.yaml deleted file mode 100644 index d50d527d127..00000000000 --- a/.changes/unreleased/ENHANCEMENTS-20250512-171131.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: ENHANCEMENTS -body: 'helper/schema: Added `RequiredForImport` and `OptionalForImport` fields to the `Schema` struct, which - are only valid for identity schemas.' -time: 2025-05-12T17:11:31.101353-04:00 -custom: - Issue: "1444" diff --git a/.changes/unreleased/ENHANCEMENTS-20250512-171419.yaml b/.changes/unreleased/ENHANCEMENTS-20250512-171419.yaml deleted file mode 100644 index abd40d6f53b..00000000000 --- a/.changes/unreleased/ENHANCEMENTS-20250512-171419.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: ENHANCEMENTS -body: 'helper/schema: Updated `ResourceData` to support passing of identity data in CRUD and import functions for managed resources.' -time: 2025-05-12T17:14:19.893664-04:00 -custom: - Issue: "1444" diff --git a/.changes/unreleased/FEATURES-20250512-170956.yaml b/.changes/unreleased/FEATURES-20250512-170956.yaml deleted file mode 100644 index 6974651724c..00000000000 --- a/.changes/unreleased/FEATURES-20250512-170956.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: FEATURES -body: 'helper/schema: Added new `TestResourceDataWithIdentityRaw` function for creating a `ResourceData` struct with identity data for unit testing.' -time: 2025-05-12T17:09:56.812031-04:00 -custom: - Issue: "1475" diff --git a/.changes/unreleased/FEATURES-20250512-171620.yaml b/.changes/unreleased/FEATURES-20250512-171620.yaml deleted file mode 100644 index 867378b68d7..00000000000 --- a/.changes/unreleased/FEATURES-20250512-171620.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: FEATURES -body: 'helper/schema: Added new `Identity` field to `Resource` that supports defining an identity schema for managed resources only.' -time: 2025-05-12T17:16:20.82808-04:00 -custom: - Issue: "1444" diff --git a/.changes/unreleased/FEATURES-20250512-172448.yaml b/.changes/unreleased/FEATURES-20250512-172448.yaml deleted file mode 100644 index 4b9f0fd6ef0..00000000000 --- a/.changes/unreleased/FEATURES-20250512-172448.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: FEATURES -body: Added new `ImportStatePassthroughWithIdentity` helper that can support both identity and ID importing via a single field. -time: 2025-05-12T17:24:48.004385-04:00 -custom: - Issue: "1474" diff --git a/.changes/unreleased/NOTES-20250317-152749.yaml b/.changes/unreleased/NOTES-20250317-152749.yaml deleted file mode 100644 index d9faaebe4ab..00000000000 --- a/.changes/unreleased/NOTES-20250317-152749.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: NOTES -body: 'all: This Go module has been updated to Go 1.23 per the [Go support policy](https://go.dev/doc/devel/release#policy). - It is recommended to review the [Go 1.23 release notes](https://go.dev/doc/go1.23) - before upgrading. Any consumers building on earlier Go versions may experience errors.' -time: 2025-03-17T15:27:49.758739-04:00 -custom: - Issue: "1445" diff --git a/.changes/unreleased/NOTES-20250512-171756.yaml b/.changes/unreleased/NOTES-20250512-171756.yaml deleted file mode 100644 index 07ced3b34a1..00000000000 --- a/.changes/unreleased/NOTES-20250512-171756.yaml +++ /dev/null @@ -1,12 +0,0 @@ -kind: NOTES -body: 'all: This release contains new fields and structs for implmenting managed resource identity. - Resource identity is data that is defined by a separate schema and is stored alongside resource state. - Identity data is used by Terrform to uniquely identify a remote object and is meant to be immutable during - the remote object''s lifecycle. Resources that support identity can now be imported using the `identity` attribute - in Terraform configuration `import` blocks, available in Terraform v1.12+. The `resource.Identity` field on the `schema.Resource` - struct can be used to support identity by defining an identity schema. Once the identity schema is defined, you can read - and store identity data in the state file with the new `IdentityData` struct that is available via the `Identity()` method - on `schema.ResourceData` and `schema.ResourceDiff` structs.' -time: 2025-05-12T17:17:56.934967-04:00 -custom: - Issue: "1444" diff --git a/CHANGELOG.md b/CHANGELOG.md index 1847bacfa34..796e0a6b1b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +## 2.37.0 (May 16, 2025) + +NOTES: + +* all: This Go module has been updated to Go 1.23 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.23 release notes](https://go.dev/doc/go1.23) before upgrading. Any consumers building on earlier Go versions may experience errors. ([#1445](https://github.com/hashicorp/terraform-plugin-sdk/issues/1445)) +* all: This release contains new fields and structs for implmenting managed resource identity. Resource identity is data that is defined by a separate schema and is stored alongside resource state. Identity data is used by Terrform to uniquely identify a remote object and is meant to be immutable during the remote object's lifecycle. Resources that support identity can now be imported using the `identity` attribute in Terraform configuration `import` blocks, available in Terraform v1.12+. The `resource.Identity` field on the `schema.Resource` struct can be used to support identity by defining an identity schema. Once the identity schema is defined, you can read and store identity data in the state file with the new `IdentityData` struct that is available via the `Identity()` method on `schema.ResourceData` and `schema.ResourceDiff` structs. ([#1444](https://github.com/hashicorp/terraform-plugin-sdk/issues/1444)) + +FEATURES: + +* helper/schema: Added new `TestResourceDataWithIdentityRaw` function for creating a `ResourceData` struct with identity data for unit testing. ([#1475](https://github.com/hashicorp/terraform-plugin-sdk/issues/1475)) +* helper/schema: Added new `Identity` field to `Resource` that supports defining an identity schema for managed resources only. ([#1444](https://github.com/hashicorp/terraform-plugin-sdk/issues/1444)) +* Added new `ImportStatePassthroughWithIdentity` helper that can support both identity and ID importing via a single field. ([#1474](https://github.com/hashicorp/terraform-plugin-sdk/issues/1474)) + +ENHANCEMENTS: + +* helper/schema: Added `RequiredForImport` and `OptionalForImport` fields to the `Schema` struct, which are only valid for identity schemas. ([#1444](https://github.com/hashicorp/terraform-plugin-sdk/issues/1444)) +* helper/schema: Updated `ResourceData` to support passing of identity data in CRUD and import functions for managed resources. ([#1444](https://github.com/hashicorp/terraform-plugin-sdk/issues/1444)) + +BUG FIXES: + +* helper/schema: Fixed bug that blocked write-only attributes from being used with resources without update functions. ([#1472](https://github.com/hashicorp/terraform-plugin-sdk/issues/1472)) + ## 2.37.0-beta.1 (April 18, 2025) NOTES: